@@ -16,7 +16,12 @@ const Validator = require('./validator');
1616const Values = require ( './values' ) ;
1717
1818
19- const internals = { } ;
19+ const internals = {
20+ standardTypes : new Set ( [ 'string' , 'number' , 'integer' , 'boolean' , 'object' , 'array' , 'null' ] ) ,
21+ jsonSchemaTarget : 'draft-2020-12' ,
22+ primitiveTypes : new Set ( [ 'string' , 'number' , 'boolean' ] ) ,
23+ nullSchema : ( ) => ( { type : 'null' } )
24+ } ;
2025
2126
2227internals . Base = class {
@@ -62,6 +67,168 @@ internals.Base = class {
6267 return Manifest . describe ( this ) ;
6368 }
6469
70+ $_jsonSchema ( mode , options = { } ) {
71+
72+ if ( options . target !== undefined &&
73+ options . target !== internals . jsonSchemaTarget ) {
74+
75+ throw new Error ( `Unsupported JSON Schema target: ${ options . target } ` ) ;
76+ }
77+
78+ const rootCall = ! options . $defs ;
79+ const defs = options . $defs ?? { } ;
80+
81+ let schema = { } ;
82+
83+ const isTypeAny = this . type === 'any' ;
84+ const isOnly = this . _flags . only ;
85+
86+ const valids = this . _valids && Array . from ( this . _valids . _values ) . filter ( ( v ) => v !== null ) ;
87+ let typesOverlap = true ;
88+
89+ // If 'only' is set, check if the allowed values' types overlap with the schema type
90+
91+ if ( valids && valids . length && isOnly && ! isTypeAny ) {
92+ const types = new Set ( valids . map ( ( v ) => typeof v ) ) ;
93+ typesOverlap = types . has ( this . type ) || ( this . type === 'date' && types . has ( 'object' ) ) ;
94+ }
95+
96+ // Set the JSON Schema 'type' if it's a standard type and there's an overlap
97+
98+ if ( ! isTypeAny && typesOverlap && internals . standardTypes . has ( this . type ) ) {
99+ schema . type = this . type ;
100+ }
101+
102+ if ( this . _flags . description ) {
103+ schema . description = this . _flags . description ;
104+ }
105+
106+ if ( this . _flags . default !== undefined && typeof this . _flags . default !== 'function' ) {
107+ schema . default = this . _flags . default ;
108+ }
109+
110+ // Apply type-specific JSON Schema conversion
111+
112+ const subOptions = { ...options , $defs : defs } ;
113+ if ( this . _definition . jsonSchema && typesOverlap ) {
114+ schema = this . _definition . jsonSchema ( this , schema , mode , subOptions ) ;
115+ }
116+
117+ // Apply rule-specific JSON Schema conversions
118+
119+ for ( const rule of this . _rules ) {
120+ const definition = this . _definition . rules [ rule . name ] ;
121+ if ( definition . jsonSchema && typesOverlap ) {
122+ schema = definition . jsonSchema ( rule , schema , isOnly , mode , subOptions ) ;
123+ }
124+ }
125+
126+ // Handle shared schemas
127+
128+ if ( this . $_terms . shared ) {
129+ for ( const shared of this . $_terms . shared ) {
130+ defs [ shared . _flags . id ] = shared . $_jsonSchema ( mode , subOptions ) ;
131+ }
132+ }
133+
134+ if ( rootCall && Object . keys ( defs ) . length ) {
135+ schema . $defs = defs ;
136+ }
137+
138+ // Handle allowed values (valids)
139+
140+ if ( this . _valids ) {
141+
142+ const values = valids . filter ( ( v ) => typeof v !== 'symbol' ) ;
143+ if ( values . length ) {
144+ if ( this . _flags . only ) {
145+ schema . enum = values ;
146+
147+ const list = Common . intersect ( new Set ( values . map ( ( v ) => typeof v ) ) , internals . primitiveTypes ) ;
148+
149+ if ( list . size ) {
150+ const types = [ ...list ] ;
151+ schema . type = types . length === 1 ? types [ 0 ] : types ;
152+ }
153+ }
154+ else {
155+ // If values are allowed but not exclusive, add them via 'anyOf' if they differ from the main type
156+
157+ const otherTypes = values . filter ( ( v ) => typeof v !== this . type || isTypeAny ) ;
158+ if ( otherTypes . length && ! ( isTypeAny && ! isOnly ) ) {
159+ if ( ! schema . anyOf ) {
160+ schema = {
161+ anyOf : [ schema ]
162+ } ;
163+ }
164+
165+ schema . anyOf . push ( { enum : otherTypes } ) ;
166+ }
167+ }
168+ }
169+ }
170+
171+ // Handle 'null' if it's an allowed value
172+
173+ if ( this . _valids && this . _valids . has ( null ) && ! ( isTypeAny && ! isOnly ) ) {
174+ if ( this . _valids . length === 1 && ( isTypeAny || isOnly ) ) {
175+ schema . type = 'null' ;
176+ }
177+ else if ( schema . type ) {
178+ schema . type = [ schema . type , 'null' ] ;
179+ }
180+ else if ( schema . anyOf ) {
181+ schema . anyOf . unshift ( internals . nullSchema ( ) ) ;
182+ }
183+ else {
184+ schema = {
185+ anyOf : [
186+ internals . nullSchema ( ) ,
187+ schema
188+ ]
189+ } ;
190+ }
191+ }
192+
193+ // Handle conditionals (whens) by generating multiple possible schemas combined with 'anyOf'
194+
195+ if ( this . $_terms . whens ) {
196+
197+ const base = this . clone ( ) ;
198+ base . $_terms . whens = null ;
199+
200+ const matches = [ ] ;
201+ for ( const when of this . $_terms . whens ) {
202+ const tests = when . is ? [ when ] : when . switch ;
203+ for ( let i = 0 ; i < tests . length ; ++ i ) {
204+ const test = tests [ i ] ;
205+ if ( test . then ) {
206+ matches . push ( base . concat ( test . then ) . $_jsonSchema ( mode , subOptions ) ) ;
207+ }
208+
209+ if ( test . otherwise ) {
210+ matches . push ( base . concat ( test . otherwise ) . $_jsonSchema ( mode , subOptions ) ) ;
211+ }
212+
213+ if ( ! test . then || ( i === tests . length - 1 && ! test . otherwise ) ) {
214+ matches . push ( base . $_jsonSchema ( mode , subOptions ) ) ;
215+ }
216+ }
217+ }
218+
219+ const results = [ ] ;
220+ for ( const match of matches ) {
221+ if ( ! results . some ( ( r ) => deepEqual ( r , match ) ) ) {
222+ results . push ( match ) ;
223+ }
224+ }
225+
226+ return { anyOf : results } ;
227+ }
228+
229+ return schema ;
230+ }
231+
65232 // Rules
66233
67234 allow ( ...values ) {
@@ -1116,6 +1283,10 @@ internals.Base = class {
11161283 }
11171284
11181285 return mapToStandardError ( result . error ) ;
1286+ } ,
1287+ jsonSchema : {
1288+ input : ( options ) => this . $_jsonSchema ( 'input' , options ) ,
1289+ output : ( options ) => this . $_jsonSchema ( 'output' , options )
11191290 }
11201291 } ;
11211292 }
0 commit comments