@@ -111,9 +111,26 @@ function isJsonSchemaNumberEncoding(schema: JsonSchema.JsonSchema): boolean {
111111 schema . anyOf . slice ( 1 ) . every ( ( member ) => member . type === "string" )
112112}
113113
114+ // Keep this allowlist closed: applicators and dependent keywords can change meaning when moved across schema objects.
115+ const inlineableCheckKeywords =
116+ "|type|format|pattern|multipleOf|minimum|maximum|exclusiveMinimum|exclusiveMaximum|minLength|maxLength|minItems|maxItems|uniqueItems|minProperties|maxProperties|propertyNames|"
117+
118+ function hasOnlyKeywords ( schema : JsonSchema . JsonSchema , allowed : string ) : boolean {
119+ return Object . keys ( schema ) . every ( ( key ) => allowed . includes ( `|${ key } |` ) )
120+ }
121+
122+ function hasNoCollisions ( left : JsonSchema . JsonSchema , rightKeys : ReadonlyArray < string > ) : boolean {
123+ return typeof left . $ref !== "string" && rightKeys . every ( ( key ) => ! Object . hasOwn ( left , key ) )
124+ }
125+
126+ // `format` and `content*` can affect validation, so they are not treated as pure annotations.
127+ const promotableAnnotationKeywords = "|title|description|default|examples|readOnly|writeOnly|"
128+ const inlineableAnnotatedCheckKeywords = inlineableCheckKeywords + promotableAnnotationKeywords
129+
114130function appendJsonSchema (
115131 left : JsonSchema . JsonSchema ,
116- right : JsonSchema . JsonSchema
132+ right : JsonSchema . JsonSchema ,
133+ inlineCheck ?: true
117134) : JsonSchema . JsonSchema {
118135 if ( Object . keys ( left ) . length === 0 ) return right
119136 const rightKeys = Object . keys ( right )
@@ -126,9 +143,17 @@ function appendJsonSchema(
126143 const type = leftType === "integer" || extracted . type === "integer" ? "integer" : "number"
127144 const base : JsonSchema . JsonSchema = { ...left , type }
128145 if ( isNumberEncoding ) delete base . anyOf
129- return Object . keys ( extracted . schema ) . length === 0 ? base : appendJsonSchema ( base , extracted . schema )
146+ const extractedKeys = Object . keys ( extracted . schema )
147+ if ( extractedKeys . length === 0 ) return base
148+ return hasOnlyKeywords ( extracted . schema , promotableAnnotationKeywords ) &&
149+ hasNoCollisions ( base , extractedKeys )
150+ ? { ...base , ...extracted . schema }
151+ : appendJsonSchema ( base , extracted . schema , inlineCheck )
130152 }
131153 }
154+ if ( inlineCheck && hasNoCollisions ( left , rightKeys ) ) {
155+ return { ...left , ...right }
156+ }
132157 const members = Array . isArray ( right . allOf ) && rightKeys . length === 1 ? right . allOf : [ right ]
133158 if ( Array . isArray ( left . allOf ) ) {
134159 return { ...left , allOf : [ ...left . allOf , ...members ] }
@@ -231,23 +256,30 @@ function compileJsonSchema(
231256 check : SchemaRepresentation . Check ,
232257 type : JsonSchema . Type | undefined ,
233258 path : Path
234- ) : JsonSchema . JsonSchema | undefined {
259+ ) : readonly [ schema : JsonSchema . JsonSchema , inline ?: true ] | undefined {
235260 const annotations = check . annotations
236261 const callback = annotations ?. toJsonSchema
237262 if ( callback !== undefined ) {
238263 const schemas = annotationSchemas ( check . representation , [ ...path , "representation" ] )
239264 const fragment = ( callback as SchemaRepresentation . ToJsonSchema . Check ) ( { type, schemas } )
240265 const ordinary = collectJsonSchemaAnnotations ( annotations , options )
241- return ordinary === undefined ? fragment : { ...fragment , ...ordinary }
266+ const schema = ordinary === undefined ? fragment : { ...fragment , ...ordinary }
267+ const allowed = ordinary === undefined ? inlineableCheckKeywords : inlineableAnnotatedCheckKeywords
268+ return check . _tag === "Filter" &&
269+ hasOnlyKeywords ( schema , allowed ) &&
270+ ( ordinary === undefined || hasOnlyKeywords ( ordinary , promotableAnnotationKeywords ) )
271+ ? [ schema , true ]
272+ : [ schema ]
242273 }
243274 if ( check . _tag === "Filter" ) return undefined
244275
245276 const children = check . checks
246277 . map ( ( child , index ) => compileCheck ( child , type , [ ...path , "checks" , index ] ) )
247- . filter ( ( child ) : child is JsonSchema . JsonSchema => child !== undefined )
278+ . filter ( ( child ) : child is NonNullable < typeof child > => child !== undefined )
248279 if ( children . length === 0 ) return undefined
249280 const ordinary = collectJsonSchemaAnnotations ( annotations , options )
250- return ordinary === undefined ? { allOf : children } : { allOf : children , ...ordinary }
281+ const allOf = children . map ( ( [ schema ] ) => schema )
282+ return [ ordinary === undefined ? { allOf } : { allOf, ...ordinary } ]
251283 }
252284
253285 function recur (
@@ -270,7 +302,7 @@ function compileJsonSchema(
270302 const type = typeof output . type === "string" && isJsonSchemaType ( output . type ) ? output . type : undefined
271303 const check = compileCheck ( representation . checks [ index ] , type , [ ...path , "checks" , index ] )
272304 if ( check !== undefined ) {
273- output = appendJsonSchema ( output , check )
305+ output = appendJsonSchema ( output , ... check )
274306 }
275307 }
276308 compiledRepresentations . set ( representation , output )
0 commit comments