| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import { Schema, type SchemaRepresentation } from "effect" |
| 3 | |
| 4 | describe("Schema.toJsonSchemaDocument", () => { |
| 5 | it("uses the encoded side for representations and JSON Schema", () => { |
| 6 | const representation = Schema.toRepresentation(Schema.FiniteFromString) |
| 7 | assert.strictEqual(representation.representation._tag, "String") |
| 8 | |
| 9 | const typeRepresentation = Schema.toRepresentation(Schema.toType(Schema.FiniteFromString)) |
| 10 | assert.strictEqual(typeRepresentation.representation._tag, "Number") |
| 11 | |
| 12 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(Schema.FiniteFromString), { |
| 13 | dialect: "draft-2020-12", |
| 14 | schema: { type: "string" }, |
| 15 | definitions: {} |
| 16 | }) |
| 17 | }) |
| 18 | |
| 19 | it("inlines repeated anonymous schemas by default", () => { |
| 20 | const shared = Schema.Struct({ value: Schema.String }) |
| 21 | |
| 22 | assert.deepStrictEqual( |
| 23 | Schema.toJsonSchemaDocument(Schema.Struct({ first: shared, second: shared })), |
| 24 | { |
| 25 | dialect: "draft-2020-12", |
| 26 | schema: { |
| 27 | type: "object", |
| 28 | properties: { |
| 29 | first: { |
| 30 | type: "object", |
| 31 | properties: { value: { type: "string" } }, |
| 32 | required: ["value"], |
| 33 | additionalProperties: true |
| 34 | }, |
| 35 | second: { |
| 36 | type: "object", |
| 37 | properties: { value: { type: "string" } }, |
| 38 | required: ["value"], |
| 39 | additionalProperties: true |
| 40 | } |
| 41 | }, |
| 42 | required: ["first", "second"], |
| 43 | additionalProperties: true |
| 44 | }, |
| 45 | definitions: {} |
| 46 | } |
| 47 | ) |
| 48 | }) |
| 49 | |
| 50 | it("forwards the reference policy to representation and JSON Schema generation", () => { |
| 51 | const shared = Schema.String.annotate({ identifier: "Shared" }) |
| 52 | const options = { referencePolicy: () => undefined } |
| 53 | |
| 54 | assert.deepStrictEqual(Schema.toRepresentation(shared, options).references, {}) |
| 55 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(shared, options), { |
| 56 | dialect: "draft-2020-12", |
| 57 | schema: { type: "string" }, |
| 58 | definitions: {} |
| 59 | }) |
| 60 | |
| 61 | const inputs: Array<SchemaRepresentation.ReferencePolicyInput> = [] |
| 62 | Schema.toJsonSchemaDocument(Schema.Date.annotate({ identifier: "Date" }), { |
| 63 | referencePolicy: (input) => { |
| 64 | inputs.push(input) |
| 65 | return undefined |
| 66 | } |
| 67 | }) |
| 68 | assert.strictEqual(inputs[0].ast._tag, "String") |
| 69 | assert.strictEqual(inputs[0].identifier, "DateEncoded") |
| 70 | }) |
| 71 | |
| 72 | it("projects encoded tuple elements for JSON Schema", () => { |
| 73 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(Schema.Tuple([Schema.NumberFromString])).schema, { |
| 74 | type: "array", |
| 75 | prefixItems: [{ type: "string" }], |
| 76 | minItems: 1, |
| 77 | maxItems: 1 |
| 78 | }) |
| 79 | }) |
| 80 | |
| 81 | it("does not project checks through an artificial JSON encoding", () => { |
| 82 | assert.deepStrictEqual( |
| 83 | Schema.toJsonSchemaDocument(Schema.Number.check(Schema.isGreaterThan(0))), |
| 84 | { |
| 85 | dialect: "draft-2020-12", |
| 86 | schema: { |
| 87 | anyOf: [ |
| 88 | { type: "number" }, |
| 89 | { |
| 90 | type: "string", |
| 91 | enum: ["Infinity", "-Infinity", "NaN"] |
| 92 | } |
| 93 | ] |
| 94 | }, |
| 95 | definitions: {} |
| 96 | } |
| 97 | ) |
| 98 | }) |
| 99 | |
| 100 | it("preserves checks when no artificial JSON encoding is needed", () => { |
| 101 | assert.deepStrictEqual( |
| 102 | Schema.toJsonSchemaDocument(Schema.Number.check(Schema.isFinite(), Schema.isGreaterThan(0))), |
| 103 | { |
| 104 | dialect: "draft-2020-12", |
| 105 | schema: { |
| 106 | type: "number", |
| 107 | exclusiveMinimum: 0 |
| 108 | }, |
| 109 | definitions: {} |
| 110 | } |
| 111 | ) |
| 112 | }) |
| 113 | |
| 114 | it("does not project annotations through an artificial JSON encoding", () => { |
| 115 | const schema = Schema.Number.annotate({ description: "source description" }) |
| 116 | const expected = { |
| 117 | dialect: "draft-2020-12" as const, |
| 118 | schema: { |
| 119 | anyOf: [ |
| 120 | { type: "number" }, |
| 121 | { type: "string", enum: ["Infinity", "-Infinity", "NaN"] } |
| 122 | ] |
| 123 | }, |
| 124 | definitions: {} |
| 125 | } |
| 126 | |
| 127 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema), expected) |
| 128 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(Schema.toCodecJson(schema)), expected) |
| 129 | }) |
| 130 | |
| 131 | it("preserves output, references and generation options", () => { |
| 132 | const shared = Schema.String.check(Schema.isMinLength(2)).annotate({ |
| 133 | identifier: "Shared", |
| 134 | description: "shared text", |
| 135 | "x-consumer": "kept" |
| 136 | }) |
| 137 | const schema = Schema.Struct({ |
| 138 | first: shared, |
| 139 | second: shared, |
| 140 | count: Schema.FiniteFromString |
| 141 | }).annotate({ description: "root" }) |
| 142 | const options: Schema.ToJsonSchemaOptions = { |
| 143 | onExcessProperty: "ignore", |
| 144 | generateDescriptions: true, |
| 145 | includeAnnotationKey: (key) => key === "x-consumer" |
| 146 | } |
| 147 | |
| 148 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema, options), { |
| 149 | dialect: "draft-2020-12", |
| 150 | schema: { |
| 151 | type: "object", |
| 152 | properties: { |
| 153 | first: { $ref: "#/$defs/Shared" }, |
| 154 | second: { $ref: "#/$defs/Shared" }, |
| 155 | count: { |
| 156 | type: "string", |
| 157 | description: "a string that will be decoded as a finite number" |
| 158 | } |
| 159 | }, |
| 160 | required: ["first", "second", "count"], |
| 161 | additionalProperties: true, |
| 162 | description: "root" |
| 163 | }, |
| 164 | definitions: { |
| 165 | Shared: { |
| 166 | type: "string", |
| 167 | allOf: [{ |
| 168 | minLength: 1, |
| 169 | description: "shared text", |
| 170 | "x-consumer": "kept" |
| 171 | }] |
| 172 | } |
| 173 | } |
| 174 | }) |
| 175 | }) |
| 176 | |
| 177 | it("uses custom compiler annotations without a central built-in switch", () => { |
| 178 | const custom = Schema.String.check(Schema.makeFilter<string>((value) => value.length >= 2, { |
| 179 | representation: { |
| 180 | id: "test/schema/minTwoCharacters", |
| 181 | payload: null |
| 182 | }, |
| 183 | toJsonSchema: () => ({ minLength: 2 }) |
| 184 | })) |
| 185 | |
| 186 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(custom), { |
| 187 | dialect: "draft-2020-12", |
| 188 | schema: { |
| 189 | type: "string", |
| 190 | minLength: 2 |
| 191 | }, |
| 192 | definitions: {} |
| 193 | }) |
| 194 | }) |
| 195 | |
| 196 | it("emits JSON content media types after encoded projection", () => { |
| 197 | const schema = Schema.fromJsonString(Schema.Struct({ |
| 198 | value: Schema.FiniteFromString |
| 199 | })) |
| 200 | |
| 201 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema).schema, { |
| 202 | type: "string", |
| 203 | contentMediaType: "application/json" |
| 204 | }) |
| 205 | }) |
| 206 | |
| 207 | it("approximates declarations without a JSON codec", () => { |
| 208 | const schema = Schema.declare((input): input is string => typeof input === "string", { |
| 209 | representation: { |
| 210 | id: "test/schema/opaqueString", |
| 211 | payload: null |
| 212 | } |
| 213 | }) |
| 214 | |
| 215 | assert.deepStrictEqual(Schema.toJsonSchemaDocument(schema), { |
| 216 | dialect: "draft-2020-12", |
| 217 | schema: {}, |
| 218 | definitions: {} |
| 219 | }) |
| 220 | }) |
| 221 | }) |