Skip to content

Commit

Permalink
JSON Schema: change default behavior for property signatures containi… (
Browse files Browse the repository at this point in the history
  • Loading branch information
gcanti authored Jul 17, 2024
1 parent 3ac2d76 commit f0285d3
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 42 deletions.
65 changes: 65 additions & 0 deletions .changeset/fuzzy-news-travel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
"@effect/schema": patch
---

JSON Schema: change default behavior for property signatures containing `undefined`

Changed the default behavior when encountering a required property signature whose type contains `undefined`. Instead of raising an exception, `undefined` is now pruned and **the field is set as optional**.

Before

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

const schema = Schema.Struct({
a: Schema.NullishOr(Schema.Number)
})

const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
throws
Error: Missing annotation
at path: ["a"]
details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation
schema (UndefinedKeyword): undefined
*/
```

Now

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

const schema = Schema.Struct({
a: Schema.NullishOr(Schema.Number)
})

const jsonSchema = JSONSchema.make(schema)
console.log(JSON.stringify(jsonSchema, null, 2))
/*
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [], // <=== empty
"properties": {
"a": {
"anyOf": [
{
"type": "number"
},
{
"$ref": "#/$defs/null"
}
]
}
},
"additionalProperties": false,
"$defs": {
"null": {
"const": null
}
}
}
*/
```
29 changes: 15 additions & 14 deletions packages/schema/src/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,12 +268,14 @@ const getJsonSchemaAnnotations = (annotated: AST.Annotated): JsonSchemaAnnotatio
default: AST.getDefaultAnnotation(annotated)
})

const pruneUndefinedKeyword = (ps: AST.PropertySignature): AST.AST => {
const pruneUndefinedKeyword = (ps: AST.PropertySignature): AST.AST | undefined => {
const type = ps.type
if (ps.isOptional && AST.isUnion(type) && Option.isNone(AST.getJSONSchemaAnnotation(type))) {
return AST.Union.make(type.types.filter((type) => !AST.isUndefinedKeyword(type)), type.annotations)
if (AST.isUnion(type) && Option.isNone(AST.getJSONSchemaAnnotation(type))) {
const types = type.types.filter((type) => !AST.isUndefinedKeyword(type))
if (types.length < type.types.length) {
return AST.Union.make(types, type.annotations)
}
}
return type
}

/** @internal */
Expand Down Expand Up @@ -473,12 +475,6 @@ const go = (
throw new Error(errors_.getJSONSchemaUnsupportedParameterErrorMessage(path, parameter))
}
}
const propertySignatures = ast.propertySignatures.map((ps) => {
return merge(
go(pruneUndefinedKeyword(ps), $defs, true, path.concat(ps.name)),
getJsonSchemaAnnotations(ps)
)
})
const output: JsonSchema7Object = {
type: "object",
required: [],
Expand All @@ -488,14 +484,19 @@ const go = (
// ---------------------------------------------
// handle property signatures
// ---------------------------------------------
for (let i = 0; i < propertySignatures.length; i++) {
const name = ast.propertySignatures[i].name
for (let i = 0; i < ast.propertySignatures.length; i++) {
const ps = ast.propertySignatures[i]
const name = ps.name
if (Predicate.isString(name)) {
output.properties[name] = propertySignatures[i]
const pruned = pruneUndefinedKeyword(ps)
output.properties[name] = merge(
go(pruned ? pruned : ps.type, $defs, true, path.concat(ps.name)),
getJsonSchemaAnnotations(ps)
)
// ---------------------------------------------
// handle optional property signatures
// ---------------------------------------------
if (!ast.propertySignatures[i].isOptional) {
if (!ps.isOptional && pruned === undefined) {
output.required.push(name)
}
} else {
Expand Down
84 changes: 56 additions & 28 deletions packages/schema/test/JSONSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -791,35 +791,63 @@ details: Cannot encode Symbol(@effect/schema/test/a) key to JSON Schema`
)
})

it("should prune `UndefinedKeyword` if the property signature is marked as optional and contains a union that includes `UndefinedKeyword`", () => {
expectJSONSchema(
Schema.Struct({
a: Schema.optional(Schema.String)
}),
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"a": {
"type": "string"
}
},
"required": [],
"additionalProperties": false
}
)
})
describe("pruning undefined", () => {
it("with an annotation the property should remain required", () => {
expectJSONSchema(
Schema.Struct({
a: Schema.UndefinedOr(Schema.String).annotations({ jsonSchema: { "type": "number" } })
}),
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["a"],
"properties": {
"a": {
"type": "number"
}
},
"additionalProperties": false
}
)
})

it("should raise an error if the property signature is not marked as optional and contains a union that includes `UndefinedKeyword`", () => {
expectError(
Schema.Struct({
a: Schema.UndefinedOr(Schema.String)
}),
`Missing annotation
at path: ["a"]
details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation
schema (UndefinedKeyword): undefined`
)
it("should prune `UndefinedKeyword` from an optional property signature", () => {
expectJSONSchema(
Schema.Struct({
a: Schema.optional(Schema.String)
}),
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"a": {
"type": "string"
}
},
"required": [],
"additionalProperties": false
}
)
})

it("should prune `UndefinedKeyword` from a required property signature type and make the property optional by default", () => {
expectJSONSchema(
Schema.Struct({
a: Schema.UndefinedOr(Schema.String)
}),
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": [],
"properties": {
"a": {
"type": "string"
}
},
"additionalProperties": false
}
)
})
})
})

Expand Down

0 comments on commit f0285d3

Please sign in to comment.