Skip to content

Commit 3b26094

Browse files
authored
Refactor JSONSchema generator
1 parent d014615 commit 3b26094

23 files changed

+5281
-815
lines changed

.changeset/bumpy-jeans-vanish.md

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
---
2+
"@effect/platform-node": patch
3+
"effect": patch
4+
---
5+
6+
## Annotation Behavior
7+
8+
When you call `.annotations` on a schema, any identifier annotations that were previously set will now be removed. Identifiers are now always tied to the schema's `ast` reference (this was the intended behavior).
9+
10+
**Example**
11+
12+
```ts
13+
import { JSONSchema, Schema } from "effect"
14+
15+
const schema = Schema.URL
16+
17+
console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
18+
/*
19+
{
20+
"$schema": "http://json-schema.org/draft-07/schema#",
21+
"$defs": {
22+
"URL": {
23+
"type": "string",
24+
"description": "a string to be decoded into a URL"
25+
}
26+
},
27+
"$ref": "#/$defs/URL"
28+
}
29+
*/
30+
31+
const annotated = Schema.URL.annotations({ description: "description" })
32+
33+
console.log(JSON.stringify(JSONSchema.make(annotated), null, 2))
34+
/*
35+
{
36+
"$schema": "http://json-schema.org/draft-07/schema#",
37+
"type": "string",
38+
"description": "description"
39+
}
40+
*/
41+
```
42+
43+
## OpenAPI 3.1 Compatibility
44+
45+
OpenAPI 3.1 does not allow `nullable: true`.
46+
Instead, the schema will now correctly use `{ "type": "null" }` inside a union.
47+
48+
**Example**
49+
50+
```ts
51+
import { JSONSchema, Schema } from "effect"
52+
53+
const schema = Schema.NullOr(Schema.String)
54+
55+
console.log(
56+
JSON.stringify(
57+
JSONSchema.fromAST(schema.ast, {
58+
definitions: {},
59+
target: "openApi3.1"
60+
}),
61+
null,
62+
2
63+
)
64+
)
65+
/*
66+
{
67+
"anyOf": [
68+
{
69+
"type": "string"
70+
},
71+
{
72+
"type": "null"
73+
}
74+
]
75+
}
76+
*/
77+
```
78+
79+
## Schema Description Deduplication
80+
81+
Previously, when a schema was reused, only the first description was kept.
82+
Now, every property keeps its own description, even if the schema is reused.
83+
84+
**Example**
85+
86+
```ts
87+
import { JSONSchema, Schema } from "effect"
88+
89+
const schemaWithAnIdentifier = Schema.String.annotations({
90+
identifier: "my-id"
91+
})
92+
93+
const schema = Schema.Struct({
94+
a: schemaWithAnIdentifier.annotations({
95+
description: "a-description"
96+
}),
97+
b: schemaWithAnIdentifier.annotations({
98+
description: "b-description"
99+
})
100+
})
101+
102+
console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
103+
/*
104+
{
105+
"$schema": "http://json-schema.org/draft-07/schema#",
106+
"type": "object",
107+
"required": [
108+
"a",
109+
"b"
110+
],
111+
"properties": {
112+
"a": {
113+
"type": "string",
114+
"description": "a-description"
115+
},
116+
"b": {
117+
"type": "string",
118+
"description": "b-description"
119+
}
120+
},
121+
"additionalProperties": false
122+
}
123+
*/
124+
```
125+
126+
## Fragment Detection in Non-Refinement Schemas
127+
128+
This patch fixes the issue where fragments (e.g. `jsonSchema.format`) were not detected on non-refinement schemas.
129+
130+
**Example**
131+
132+
```ts
133+
import { JSONSchema, Schema } from "effect"
134+
135+
const schema = Schema.UUID.pipe(
136+
Schema.compose(Schema.String),
137+
Schema.annotations({
138+
identifier: "UUID",
139+
title: "title",
140+
description: "description",
141+
jsonSchema: {
142+
format: "uuid" // fragment
143+
}
144+
})
145+
)
146+
147+
console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
148+
/*
149+
{
150+
"$schema": "http://json-schema.org/draft-07/schema#",
151+
"$defs": {
152+
"UUID": {
153+
"type": "string",
154+
"description": "description",
155+
"format": "uuid",
156+
"pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
157+
"title": "title"
158+
}
159+
},
160+
"$ref": "#/$defs/UUID"
161+
}
162+
*/
163+
```
164+
165+
## Nested Unions
166+
167+
Nested unions are no longer flattened. Instead, they remain as nested `anyOf` arrays.
168+
This is fine because JSON Schema allows nested `anyOf`.
169+
170+
**Example**
171+
172+
```ts
173+
import { JSONSchema, Schema } from "effect"
174+
175+
const schema = Schema.Union(
176+
Schema.NullOr(Schema.String),
177+
Schema.Literal("a", null)
178+
)
179+
180+
console.log(JSON.stringify(JSONSchema.make(schema), null, 2))
181+
/*
182+
{
183+
"$schema": "http://json-schema.org/draft-07/schema#",
184+
"anyOf": [
185+
{
186+
"anyOf": [
187+
{
188+
"type": "string"
189+
},
190+
{
191+
"type": "null"
192+
}
193+
]
194+
},
195+
{
196+
"anyOf": [
197+
{
198+
"type": "string",
199+
"enum": [
200+
"a"
201+
]
202+
},
203+
{
204+
"type": "null"
205+
}
206+
]
207+
}
208+
]
209+
}
210+
*/
211+
```
212+
213+
## Refinements without `jsonSchema` annotation
214+
215+
Refinements that don't provide a `jsonSchema` annotation no longer cause errors.
216+
They are simply ignored, so you can still generate a JSON Schema even when refinements can't easily be expressed.

packages/effect/src/Arbitrary.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,10 +407,12 @@ export const getDescription = wrapGetDescription(
407407
case "NumberKeyword": {
408408
const c = schemaId === schemaId_.NonNaNSchemaId ?
409409
makeNumberConstraints({ noNaN: true }) :
410+
schemaId === schemaId_.FiniteSchemaId || schemaId === schemaId_.JsonNumberSchemaId ?
411+
makeNumberConstraints({ noDefaultInfinity: true, noNaN: true }) :
410412
makeNumberConstraints({
411413
isInteger: "type" in meta && meta.type === "integer",
412-
noNaN: "type" in meta && meta.type === "number" ? true : undefined,
413-
noDefaultInfinity: "type" in meta && meta.type === "number" ? true : undefined,
414+
noNaN: undefined,
415+
noDefaultInfinity: undefined,
414416
min: meta.exclusiveMinimum ?? meta.minimum,
415417
minExcluded: "exclusiveMinimum" in meta ? true : undefined,
416418
max: meta.exclusiveMaximum ?? meta.maximum,

0 commit comments

Comments
 (0)