Skip to content

Commit 03a8571

Browse files
committed
Compact JSON Schema checks
Conservatively inline independent check constraints and pure annotations without collisions, while keeping dependent schemas scoped. Prevent pnpm from automatically rebuilding incompatible node_modules.
1 parent 770c6d0 commit 03a8571

7 files changed

Lines changed: 371 additions & 161 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Compact JSON Schema check constraints when they can be safely merged without keyword collisions.

packages/effect/src/internal/schema/toJsonSchemaDocument.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
114130
function 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)

packages/effect/test/schema/representation/schemaToJsonSchemaDocument.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe("Schema.toJsonSchemaDocument", () => {
3434
anyOf: [
3535
{
3636
type: "number",
37-
allOf: [{ exclusiveMinimum: 0 }]
37+
exclusiveMinimum: 0
3838
},
3939
{
4040
type: "string",
@@ -106,7 +106,7 @@ describe("Schema.toJsonSchemaDocument", () => {
106106
dialect: "draft-2020-12",
107107
schema: {
108108
type: "string",
109-
allOf: [{ minLength: 2 }]
109+
minLength: 2
110110
},
111111
definitions: {}
112112
})

0 commit comments

Comments
 (0)