diff --git a/.changeset/silly-grapes-sleep.md b/.changeset/silly-grapes-sleep.md new file mode 100644 index 0000000000..bdc37589b8 --- /dev/null +++ b/.changeset/silly-grapes-sleep.md @@ -0,0 +1,39 @@ +--- +"@effect/platform": patch +"@effect/schema": patch +--- + +JSON Schema: handle refinements where the 'from' part includes a transformation, closes #3662 + +Before + +```ts +import { JSONSchema, Schema } from "@effect/schema" + +const schema = Schema.Date + +console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) +/* +throws +Error: Missing annotation +details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation +schema (Refinement): Date +*/ +``` + +After + +```ts +import { JSONSchema, Schema } from "@effect/schema" + +const schema = Schema.Date + +console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) +/* +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string that will be parsed into a Date" +} +*/ +``` diff --git a/.changeset/thirty-moose-pretend.md b/.changeset/thirty-moose-pretend.md new file mode 100644 index 0000000000..ee21e4fb69 --- /dev/null +++ b/.changeset/thirty-moose-pretend.md @@ -0,0 +1,38 @@ +--- +"@effect/schema": patch +--- + +Add description annotation to the encoded part of DateFromString. + +Before + +```ts +import { JSONSchema, Schema } from "@effect/schema" + +const schema = Schema.DateFromString + +console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) +/* +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string" +} +*/ +``` + +After + +```ts +import { JSONSchema, Schema } from "@effect/schema" + +const schema = Schema.DateFromString + +console.log(JSON.stringify(JSONSchema.make(schema), null, 2)) +/* +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string that will be parsed into a Date" +} +*/ +``` diff --git a/packages/platform/src/OpenApiJsonSchema.ts b/packages/platform/src/OpenApiJsonSchema.ts index abf72c72f7..f0e0b1331d 100644 --- a/packages/platform/src/OpenApiJsonSchema.ts +++ b/packages/platform/src/OpenApiJsonSchema.ts @@ -333,7 +333,7 @@ const go = ( ...getJsonSchemaAnnotations(ast) } } - if (handleIdentifier && !AST.isTransformation(ast)) { + if (handleIdentifier && !AST.isTransformation(ast) && !AST.isRefinement(ast)) { const identifier = getJSONIdentifier(ast) if (Option.isSome(identifier)) { const id = identifier.value @@ -573,7 +573,10 @@ const go = ( } } case "Refinement": { - throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + if (AST.encodedBoundAST(ast) === ast) { + throw new Error(getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + } + return go(ast.from, $defs, true, path) } case "TemplateLiteral": { const regex = AST.getTemplateLiteralRegExp(ast) diff --git a/packages/platform/test/OpenApiJsonSchema.test.ts b/packages/platform/test/OpenApiJsonSchema.test.ts index d12b4f75ff..8384e72e4b 100644 --- a/packages/platform/test/OpenApiJsonSchema.test.ts +++ b/packages/platform/test/OpenApiJsonSchema.test.ts @@ -2167,4 +2167,26 @@ schema (Suspend): ` } ) }) + + it("DateFromString", () => { + expectJSONSchema( + Schema.DateFromString, + { + "type": "string", + "title": "string", + "description": "a string that will be parsed into a Date" + } + ) + }) + + it("Date", () => { + expectJSONSchema( + Schema.Date, + { + "type": "string", + "title": "string", + "description": "a string that will be parsed into a Date" + } + ) + }) }) diff --git a/packages/schema/src/JSONSchema.ts b/packages/schema/src/JSONSchema.ts index 1cb58ef6ea..e7e0de9330 100644 --- a/packages/schema/src/JSONSchema.ts +++ b/packages/schema/src/JSONSchema.ts @@ -347,7 +347,7 @@ const go = ( if (Option.isSome(surrogate)) { return go(surrogate.value, $defs, handleIdentifier, path) } - if (handleIdentifier && !AST.isTransformation(ast)) { + if (handleIdentifier && !AST.isTransformation(ast) && !AST.isRefinement(ast)) { const identifier = AST.getJSONIdentifier(ast) if (Option.isSome(identifier)) { const id = identifier.value @@ -550,7 +550,10 @@ const go = ( }, getJsonSchemaAnnotations(ast)) } case "Refinement": { - throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + if (AST.encodedBoundAST(ast) === ast) { + throw new Error(errors_.getJSONSchemaMissingAnnotationErrorMessage(path, ast)) + } + return go(ast.from, $defs, true, path) } case "TemplateLiteral": { const regex = AST.getTemplateLiteralRegExp(ast) diff --git a/packages/schema/src/Schema.ts b/packages/schema/src/Schema.ts index 4bafa0c122..b52b3aca26 100644 --- a/packages/schema/src/Schema.ts +++ b/packages/schema/src/Schema.ts @@ -6127,7 +6127,7 @@ export class ValidDateFromSelf extends DateFromSelf.pipe( * @since 0.67.0 */ export class DateFromString extends transform( - String$, + String$.annotations({ description: "a string that will be parsed into a Date" }), DateFromSelf, { strict: true, decode: (s) => new Date(s), encode: (d) => util_.formatDate(d) } ).annotations({ identifier: "DateFromString" }) {} diff --git a/packages/schema/test/JSONSchema.test.ts b/packages/schema/test/JSONSchema.test.ts index e9c55c2bb2..cc93bda672 100644 --- a/packages/schema/test/JSONSchema.test.ts +++ b/packages/schema/test/JSONSchema.test.ts @@ -2260,6 +2260,28 @@ schema (Suspend): ` } ) }) + + it("DateFromString", () => { + expectJSONSchema( + Schema.DateFromString, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string that will be parsed into a Date" + } + ) + }) + + it("Date", () => { + expectJSONSchema( + Schema.Date, + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "string", + "description": "a string that will be parsed into a Date" + } + ) + }) }) export const decode = (schema: JSONSchema.JsonSchema7Root): Schema.Schema =>