1+ /**
2+ * Converts a JSON Schema definition to a Protocol Buffers definition.
3+ * @param jsonSchema - The JSON Schema definition to convert
4+ * @returns String containing the Protocol Buffers definition
5+ */
6+ function jsonSchemaToProtobuf ( jsonSchema : any ) : string {
7+ // Track message definitions to avoid duplicates
8+ const messages : Map < string , string > = new Map ( ) ;
9+ // Track enums definitions to avoid duplicates
10+ const enums : Map < string , string > = new Map ( ) ;
11+ // Keep track of the current indentation level
12+ let indent = 0 ;
13+
14+ /**
15+ * Helper function to indent lines
16+ */
17+ function getIndent ( ) : string {
18+ return ' ' . repeat ( indent ) ;
19+ }
20+
21+ /**
22+ * Converts a JSON Schema type to a Protobuf type
23+ */
24+ function mapType ( schema : any ) : string {
25+ if ( ! schema || ! schema . type ) {
26+ return 'google.protobuf.Any' ;
27+ }
28+
29+ switch ( schema . type ) {
30+ case 'string' :
31+ if ( schema . format === 'date-time' ) {
32+ return 'google.protobuf.Timestamp' ;
33+ } else if ( schema . enum ) {
34+ // Enums will be handled separately
35+ return schema . title || 'Enum' ;
36+ }
37+ return 'string' ;
38+ case 'integer' :
39+ return 'int32' ;
40+ case 'number' :
41+ return 'double' ;
42+ case 'boolean' :
43+ return 'bool' ;
44+ case 'null' :
45+ return 'google.protobuf.NullValue' ;
46+ case 'array' :
47+ if ( schema . items ) {
48+ const itemType = mapType ( schema . items ) ;
49+ return `repeated ${ itemType } ` ;
50+ }
51+ return 'repeated google.protobuf.Any' ;
52+ case 'object' :
53+ if ( schema . title ) {
54+ // Create a new message type
55+ return schema . title ;
56+ }
57+ return 'google.protobuf.Struct' ;
58+ default :
59+ return 'google.protobuf.Any' ;
60+ }
61+ }
62+
63+ /**
64+ * Converts a JSON Schema property name to a valid Protobuf field name
65+ */
66+ function toFieldName ( name : string ) : string {
67+ // Replace invalid characters and convert to snake_case
68+ return name
69+ . replace ( / [ ^ \w ] / g, '_' )
70+ . replace ( / ( [ A - Z ] ) / g, '_$1' )
71+ . toLowerCase ( )
72+ . replace ( / ^ _ / , '' ) ;
73+ }
74+
75+ /**
76+ * Process JSON Schema object and generate a Protobuf message
77+ */
78+ function processObject ( schema : any , name : string = 'Root' ) : string {
79+ const messageName = name ;
80+
81+ // Skip if we've already processed this message
82+ if ( messages . has ( messageName ) ) {
83+ return messageName ;
84+ }
85+
86+ let fields : string [ ] = [ ] ;
87+ let fieldNumber = 1 ;
88+
89+ indent ++ ;
90+
91+ // Process properties
92+ if ( schema . properties ) {
93+ for ( const [ propName , propSchema ] of Object . entries < any > ( schema . properties ) ) {
94+ const fieldName = toFieldName ( propName ) ;
95+ let fieldType = '' ;
96+
97+ // Handle nested objects
98+ if ( propSchema . type === 'object' && propSchema . properties ) {
99+ const nestedMessageName = processObject (
100+ propSchema ,
101+ propSchema . title || `${ messageName } ${ propName } `
102+ ) ;
103+ fieldType = nestedMessageName ;
104+ }
105+ // Handle enums
106+ else if ( propSchema . type === 'string' && propSchema . enum ) {
107+ const enumName = ( propSchema . title || `${ messageName } ${ propName } Enum` ) ;
108+ processEnum ( propSchema . enum , enumName ) ;
109+ fieldType = enumName ;
110+ }
111+ // Handle arrays with object items
112+ else if ( propSchema . type === 'array' && propSchema . items && propSchema . items . type === 'object' ) {
113+ const nestedMessageName = processObject (
114+ propSchema . items ,
115+ propSchema . items . title || `${ messageName } ${ propName } Item`
116+ ) ;
117+ fieldType = `repeated ${ nestedMessageName } ` ;
118+ }
119+ // Handle regular types
120+ else {
121+ fieldType = mapType ( propSchema ) ;
122+ }
123+
124+ // Check if property is required
125+ const isRequired = schema . required && schema . required . includes ( propName ) ;
126+
127+ // Add field definition
128+ fields . push ( `${ getIndent ( ) } ${ fieldType } ${ fieldName } = ${ fieldNumber } ;${ isRequired ? '' : ' // optional' } ` ) ;
129+ fieldNumber ++ ;
130+ }
131+ }
132+
133+ indent -- ;
134+
135+ const messageDefinition = `message ${ messageName } {\n${ fields . join ( '\n' ) } \n${ getIndent ( ) } }` ;
136+ messages . set ( messageName , messageDefinition ) ;
137+
138+ return messageName ;
139+ }
140+
141+ /**
142+ * Process JSON Schema enum and generate a Protobuf enum
143+ */
144+ function processEnum ( enumValues : string [ ] , enumName : string ) : string {
145+ // Skip if we've already processed this enum
146+ if ( enums . has ( enumName ) ) {
147+ return enumName ;
148+ }
149+
150+ let items : string [ ] = [ ] ;
151+ indent ++ ;
152+
153+ // Add the UNSPECIFIED value as first item (protobuf best practice)
154+ items . push ( `${ getIndent ( ) } ${ enumName . toUpperCase ( ) } _UNSPECIFIED = 0;` ) ;
155+
156+ // Process enum values
157+ enumValues . forEach ( ( value , index ) => {
158+ const valueName = value
159+ . toString ( )
160+ . replace ( / [ ^ \w ] / g, '_' )
161+ . toUpperCase ( ) ;
162+ items . push ( `${ getIndent ( ) } ${ valueName } = ${ index + 1 } ;` ) ;
163+ } ) ;
164+
165+ indent -- ;
166+
167+ const enumDefinition = `enum ${ enumName } {\n${ items . join ( '\n' ) } \n${ getIndent ( ) } }` ;
168+ enums . set ( enumName , enumDefinition ) ;
169+
170+ return enumName ;
171+ }
172+
173+ // Start with the syntax declaration and imports
174+ let result = 'syntax = "proto3";\n\n' ;
175+ result += 'import "google/protobuf/any.proto";\n' ;
176+ result += 'import "google/protobuf/struct.proto";\n' ;
177+ result += 'import "google/protobuf/timestamp.proto";\n\n' ;
178+
179+ // Add package declaration (optional)
180+ if ( jsonSchema . title ) {
181+ result += `package ${ jsonSchema . title . toLowerCase ( ) . replace ( / [ ^ \w ] / g, '.' ) } ;\n\n` ;
182+ } else {
183+ result += 'package schema;\n\n' ;
184+ }
185+
186+ // Generate the main message
187+ processObject ( jsonSchema , jsonSchema . title ) ;
188+
189+ // Add all enum definitions
190+ if ( enums . size > 0 ) {
191+ result += '// Enum definitions\n' ;
192+ for ( const enumDef of enums . values ( ) ) {
193+ result += enumDef + '\n\n' ;
194+ }
195+ }
196+
197+ // Add all message definitions
198+ if ( messages . size > 0 ) {
199+ result += '// Message definitions\n' ;
200+ for ( const messageDef of messages . values ( ) ) {
201+ result += messageDef + '\n\n' ;
202+ }
203+ }
204+
205+ return result . trim ( ) ;
206+ }
207+
208+ export default jsonSchemaToProtobuf ;
0 commit comments