Skip to content
This repository has been archived by the owner on Jan 6, 2025. It is now read-only.

Commit

Permalink
ensure that JSON Schema annotations can be exclusively applied to ref…
Browse files Browse the repository at this point in the history
…inements
  • Loading branch information
gcanti committed Dec 10, 2023
1 parent b8d6e0e commit 1370ad9
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-lemons-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/schema": patch
---

ensure that JSON Schema annotations can be exclusively applied to refinements
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ Output:

## Generating JSON Schemas

The `to` function, which is part of the `@effect/schema/JSONSchema` module, allows you to generate a JSON Schema based on a schema definition:
The `to` / `from` functions, which are part of the `@effect/schema/JSONSchema` module, allow you to generate a JSON Schema based on a schema definition:

```ts
import * as JSONSchema from "@effect/schema/JSONSchema";
Expand Down Expand Up @@ -818,6 +818,43 @@ In the example above, we define a schema for a "Category" that can contain a "na

This ensures that the JSON Schema properly handles the recursive structure and creates distinct definitions for each annotated schema, improving readability and maintainability.

### JSON Schema Annotations

When defining a **refinement** (e.g., through the `filter` function), you can attach a JSON Schema annotation to your schema containing a JSON Schema "fragment" related to this particular refinement. This fragment will be used to generate the corresponding JSON Schema. Note that if the schema consists of more than one refinement, the corresponding annotations will be merged.

```ts
import * as JSONSchema from "@effect/schema/JSONSchema";
import * as Schema from "@effect/schema/Schema";

// Simulate one or more refinements
const Positive = Schema.number.pipe(
Schema.filter((n) => n > 0, {
jsonSchema: { minimum: 0 },
})
);

const schema = Positive.pipe(
Schema.filter((n) => n <= 10, {
jsonSchema: { maximum: 10 },
})
);

console.log(JSONSchema.to(schema));
/*
Output:
{
'$schema': 'http://json-schema.org/draft-07/schema#',
type: 'number',
description: 'a number',
title: 'number',
minimum: 0,
maximum: 10
}
*/
```

As seen in the example, the JSON Schema annotations are merged with the base JSON Schema from `Schema.number`. This approach helps handle multiple refinements while maintaining clarity in your code.

## Generating Equivalences

The `to` function, which is part of the `@effect/schema/Equivalence` module, allows you to generate an [Equivalence](https://effect-ts.github.io/effect/modules/Equivalence.ts.html) based on a schema definition:
Expand Down
9 changes: 9 additions & 0 deletions docs/modules/Schema.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,10 @@ Added in v1.0.0
## jsonSchema
Attaches a JSON Schema annotation to a schema that represents a refinement.
If the schema is composed of more than one refinement, the corresponding annotations will be merged.
**Signature**
```ts
Expand Down Expand Up @@ -4029,6 +4033,11 @@ Added in v1.0.0
```ts
export interface FilterAnnotations<A> extends DocAnnotations<A> {
readonly typeId?: AST.TypeAnnotation | { id: AST.TypeAnnotation; params: unknown }
/**
* Attaches a JSON Schema annotation to this refinement.
*
* If the schema is composed of more than one refinement, the corresponding annotations will be merged.
*/
readonly jsonSchema?: AST.JSONSchemaAnnotation
readonly arbitrary?: (...args: ReadonlyArray<Arbitrary<any>>) => Arbitrary<any>
readonly pretty?: (...args: ReadonlyArray<Pretty<any>>) => Pretty<any>
Expand Down
8 changes: 1 addition & 7 deletions src/JSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,7 @@ export const DEFINITION_PREFIX = "#/$defs/"
const go = (ast: AST.AST, $defs: Record<string, JsonSchema7>): JsonSchema7 => {
switch (ast._tag) {
case "Declaration": {
const annotation = AST.getJSONSchemaAnnotation(ast)
if (Option.isSome(annotation)) {
return annotation.value as any
}
throw new Error(
"cannot build a JSON Schema for declarations without a JSON Schema annotation"
)
throw new Error("cannot convert a declaration to JSON Schema")
}
case "Literal": {
const literal = ast.literal
Expand Down
17 changes: 15 additions & 2 deletions src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1496,6 +1496,11 @@ export interface DocAnnotations<A> extends AST.Annotations {
*/
export interface FilterAnnotations<A> extends DocAnnotations<A> {
readonly typeId?: AST.TypeAnnotation | { id: AST.TypeAnnotation; params: unknown }
/**
* Attaches a JSON Schema annotation to this refinement.
*
* If the schema is composed of more than one refinement, the corresponding annotations will be merged.
*/
readonly jsonSchema?: AST.JSONSchemaAnnotation
readonly arbitrary?: (...args: ReadonlyArray<Arbitrary<any>>) => Arbitrary<any>
readonly pretty?: (...args: ReadonlyArray<Pretty<any>>) => Pretty<any>
Expand Down Expand Up @@ -1569,12 +1574,20 @@ export const documentation =
make(AST.setAnnotation(self.ast, AST.DocumentationAnnotationId, documentation))

/**
* Attaches a JSON Schema annotation to a schema that represents a refinement.
*
* If the schema is composed of more than one refinement, the corresponding annotations will be merged.
*
* @category annotations
* @since 1.0.0
*/
export const jsonSchema =
(jsonSchema: AST.JSONSchemaAnnotation) => <I, A>(self: Schema<I, A>): Schema<I, A> =>
make(AST.setAnnotation(self.ast, AST.JSONSchemaAnnotationId, jsonSchema))
(jsonSchema: AST.JSONSchemaAnnotation) => <I, A>(self: Schema<I, A>): Schema<I, A> => {
if (AST.isRefinement(self.ast)) {
return make(AST.setAnnotation(self.ast, AST.JSONSchemaAnnotationId, jsonSchema))
}
throw new Error("JSON Schema annotations can be applied exclusively to refinements")
}

/**
* @category annotations
Expand Down
26 changes: 5 additions & 21 deletions test/JSONSchema.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as A from "@effect/schema/Arbitrary"
import * as AST from "@effect/schema/AST"
import * as JSONSchema from "@effect/schema/JSONSchema"
import * as ParseResult from "@effect/schema/ParseResult"
import * as Schema from "@effect/schema/Schema"
import AjvNonEsm from "ajv"
import * as Option from "effect/Option"
Expand Down Expand Up @@ -74,26 +73,11 @@ describe("JSONSchema", () => {
propertyFrom(Schema.struct({ a: Schema.string, b: Schema.NumberFromString }))
})

describe("declaration", () => {
it("should raise an error when an annotation doesn't exist", () => {
const schema = Schema.chunk(JsonNumber)
expect(() => JSONSchema.to(schema)).toThrow(
new Error("cannot build a JSON Schema for declarations without a JSON Schema annotation")
)
})

it("should return the provided JSON Schema when an annotation exists", () => {
const schema = Schema.declare(
[],
Schema.struct({}),
() => (input) => ParseResult.succeed(input),
{
[AST.JSONSchemaAnnotationId]: { type: "string" },
[A.ArbitraryHookId]: (): A.Arbitrary<string> => (fc) => fc.string()
}
)
propertyTo(schema)
})
it("declaration should raise an error", () => {
const schema = Schema.chunk(JsonNumber)
expect(() => JSONSchema.to(schema)).toThrow(
new Error("cannot convert a declaration to JSON Schema")
)
})

it("bigint should raise an error", () => {
Expand Down
12 changes: 5 additions & 7 deletions test/Schema/annotations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,12 @@ describe("Schema/annotations", () => {
expect(S.isSchema(schema)).toEqual(true)
})

it("jsonSchema", () => {
const schema = S.string.pipe(S.jsonSchema({ type: "string" }))
expect(schema.ast.annotations).toEqual({
[AST.JSONSchemaAnnotationId]: { type: "string" },
[AST.TitleAnnotationId]: "string",
[AST.DescriptionAnnotationId]: "a string"
describe("jsonSchema", () => {
it("should raise an error on non refinements", () => {
expect(() => S.string.pipe(S.jsonSchema({ type: "number" }))).toThrow(
new Error("JSON Schema annotations can be applied exclusively to refinements")
)
})
expect(S.isSchema(schema)).toEqual(true)
})

it("message as annotation options", async () => {
Expand Down

0 comments on commit 1370ad9

Please sign in to comment.