Skip to content

Commit

Permalink
Fix: Correct Handling of JSON Schema Annotations in Refinements (#3284)
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jul 17, 2024
1 parent a91a8e5 commit 3ac2d76
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 17 deletions.
41 changes: 41 additions & 0 deletions .changeset/neat-spoons-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
"@effect/schema": patch
---

Fix: Correct Handling of JSON Schema Annotations in Refinements

Fixes an issue where the JSON schema annotation set by a refinement after a transformation was mistakenly interpreted as an override annotation. This caused the output to be incorrect, as the annotations were not applied as intended.

Before

```ts
import { JSONSchema, Schema } from "@effect/schema"

const schema = Schema.Trim.pipe(Schema.nonEmpty())

const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
{
"$schema": "http://json-schema.org/draft-07/schema#",
"minLength": 1
}
*/
```

Now

```ts
import { JSONSchema, Schema } from "@effect/schema"

const schema = Schema.Trim.pipe(Schema.nonEmpty())

const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
}
*/
```
39 changes: 23 additions & 16 deletions packages/schema/src/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,22 +281,19 @@ export const DEFINITION_PREFIX = "#/$defs/"

const get$ref = (id: string): string => `${DEFINITION_PREFIX}${id}`

const hasTransformation = (ast: AST.Refinement): boolean => {
const getRefinementInnerTransformation = (ast: AST.Refinement): AST.AST | undefined => {
switch (ast.from._tag) {
case "Transformation":
return true
return ast.from
case "Refinement":
return hasTransformation(ast.from)
case "Suspend":
{
const from = ast.from.f()
if (AST.isRefinement(from)) {
return hasTransformation(from)
}
return getRefinementInnerTransformation(ast.from)
case "Suspend": {
const from = ast.from.f()
if (AST.isRefinement(from)) {
return getRefinementInnerTransformation(from)
}
break
}
}
return false
}

const isParseJsonTransformation = (ast: AST.AST): boolean =>
Expand All @@ -309,6 +306,11 @@ function merge(a: object, b: object): object {
return { ...a, ...b }
}

const isOverrideAnnotation = (jsonSchema: JsonSchema7): boolean => {
return ("type" in jsonSchema) || ("oneOf" in jsonSchema) || ("anyOf" in jsonSchema) || ("const" in jsonSchema) ||
("enum" in jsonSchema) || ("$ref" in jsonSchema)
}

const go = (
ast: AST.AST,
$defs: Record<string, JsonSchema7>,
Expand All @@ -318,11 +320,16 @@ const go = (
const hook = AST.getJSONSchemaAnnotation(ast)
if (Option.isSome(hook)) {
const handler = hook.value as JsonSchema7
if (AST.isRefinement(ast) && !hasTransformation(ast)) {
try {
return merge(merge(go(ast.from, $defs, true, path), getJsonSchemaAnnotations(ast)), handler)
} catch (e) {
return merge(getJsonSchemaAnnotations(ast), handler)
if (AST.isRefinement(ast)) {
const t = getRefinementInnerTransformation(ast)
if (t === undefined) {
try {
return merge(merge(go(ast.from, $defs, true, path), getJsonSchemaAnnotations(ast)), handler)
} catch (e) {
return merge(getJsonSchemaAnnotations(ast), handler)
}
} else if (!isOverrideAnnotation(handler)) {
return go(t, $defs, true, path)
}
}
return handler
Expand Down
47 changes: 46 additions & 1 deletion packages/schema/test/JSONSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1866,12 +1866,57 @@ schema (Suspend): <suspended schema>`
}, false)
})

it("refinement of a transformation", () => {
it("refinement of a transformation with an override annotation", () => {
expectJSONSchema(Schema.Date.annotations({ jsonSchema: { type: "string", format: "date-time" } }), {
"$schema": "http://json-schema.org/draft-07/schema#",
"format": "date-time",
"type": "string"
}, false)
expectJSONSchema(
Schema.Date.annotations({
jsonSchema: { oneOf: [{ type: "object" }, { type: "array" }] }
}),
{ "$schema": "http://json-schema.org/draft-07/schema#", oneOf: [{ type: "object" }, { type: "array" }] },
false
)
expectJSONSchema(
Schema.Date.annotations({
jsonSchema: { anyOf: [{ type: "object" }, { type: "array" }] }
}),
{ "$schema": "http://json-schema.org/draft-07/schema#", anyOf: [{ type: "object" }, { type: "array" }] },
false
)
expectJSONSchema(Schema.Date.annotations({ jsonSchema: { $ref: "x" } }), {
"$schema": "http://json-schema.org/draft-07/schema#",
$ref: "x"
}, false)
expectJSONSchema(Schema.Date.annotations({ jsonSchema: { const: 1 } }), {
"$schema": "http://json-schema.org/draft-07/schema#",
const: 1
}, false)
expectJSONSchema(Schema.Date.annotations({ jsonSchema: { enum: [1] } }), {
"$schema": "http://json-schema.org/draft-07/schema#",
enum: [1]
}, false)
})

it("refinement of a transformation without an override annotation", () => {
expectJSONSchema(Schema.Trim.pipe(Schema.nonEmpty()), {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
}, false)
expectJSONSchema(Schema.Trim.pipe(Schema.nonEmpty({ jsonSchema: { title: "Description" } })), {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
}, false)
expectJSONSchema(
Schema.Trim.pipe(Schema.nonEmpty()).annotations({ jsonSchema: { title: "Description" } }),
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "string"
},
false
)
})
})

Expand Down

0 comments on commit 3ac2d76

Please sign in to comment.