Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSONSchema.make does not have same behaviour as JSONSchema.to => erasing properties & not working for some Schema types #3283

Closed
mignotju opened this issue Jul 17, 2024 · 7 comments
Assignees
Labels
bug Something isn't working schema: jsonschema schema

Comments

@mignotju
Copy link

mignotju commented Jul 17, 2024

What version of Effect is running?

3.5.1

What steps can reproduce the bug?

Hello,
I am trying to use JSONSchema.make and I have 2 issues according to my expectations:

  1. Json schema properties erased:
const schema = Schema.Struct({
  firstName: Schema.String.annotations({
    title: 'First name',
  }),
  lastName: Schema.Trim.pipe(Schema.nonEmpty()),
  description: Schema.Trim.pipe(Schema.nonEmpty()).annotations({
    jsonSchema: { title: 'Description' },
  }),
});

const jsonSchema = JSONSchema.make(schema);
console.log(jsonSchema);

The output is:

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: [ 'firstName', 'lastName', 'description' ],
  properties: {
    firstName: { type: 'string', description: 'a string', title: 'First name' }, // ✅ Great
    lastName: { minLength: 1 }, // ❌ I would expect more information, see below in expected behaviour section
    description: { title: 'Description' } // ❌ I would expect more information, see below in expected behaviour section
  },
  additionalProperties: false
}
  1. Json schema not working for some Schema types
    What I do:
// a. Schema.NullishOf
const schema = Schema.Struct({
  numberOfElements: Schema.NullishOr(Schema.Number),
});
const jsonSchema = JSONSchema.make(schema);
console.log(jsonSchema);

OR
// b. Schema.DateFromSelf
const schema = Schema.Struct({
  startDate: Schema.DateFromSelf
});
const jsonSchema = JSONSchema.make(schema);
console.log(jsonSchema);

The output is:

// a. Schema.NullishOf

Missing annotation 
at path: ["numberOfElements"] 
details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation 
schema (UndefinedKeyword): undefined 

OR

// b. Schema.DateFromSelf

Missing annotation 
at path: ["startDate"] 
details: Generating a JSON Schema for this schema requires a "jsonSchema" annotation 
schema (Declaration): DateFromSelf 

What is the expected behavior?

  1. Json schema properties erased:

According to the documentation here, here is the output I would expect:

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: [ 'firstName', 'lastName', 'description' ],
  properties: {
    firstName: { type: 'string', description: 'a string', title: 'First name' }, 
    lastName: { **type: 'string'**, minLength: 1 },
    description: { **type: 'string', title: 'Description'**, minLength: 1, } 
  },
  additionalProperties: false
}
  1. Json schema not working for some Schema types
    I would expect:
// a. Schema.NullishOf

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: [], // empty
  properties: {
    numberOfElements: { type: 'number' }, 
  },
  additionalProperties: false
}

// b. Schema.DateFromSelf

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: ['startDate'], 
  properties: {
    startDate: { type: 'date' }, 
  },
  additionalProperties: false
}

What do you see instead?

// Already answered above

Additional information

I would like to know why:

  1. JSON schema properties are erased, whereas it was not the case before with JSONSchema.to => the previous behaviour would be very beneficial for my need
  2. It is not working with some schema types

Thanks a lot for your time!

@mignotju mignotju added the bug Something isn't working label Jul 17, 2024
@gcanti gcanti self-assigned this Jul 17, 2024
@gcanti
Copy link
Contributor

gcanti commented Jul 17, 2024

According to the documentation here, here is the output I would expect:

That documentation is outdated and part of an archived repository. The updated documentation can be found here: https://github.com/Effect-TS/effect/tree/main/packages/schema

Regarding 1. I can confirm that there's a bug. However, the expected output should be:

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

const schema = Schema.Struct({
  firstName: Schema.String.annotations({
    title: "First name"
  }),
  lastName: Schema.Trim.pipe(Schema.nonEmpty()),
  description: Schema.Trim.pipe(Schema.nonEmpty()).annotations({
    jsonSchema: { title: "Description" }
  })
})

const jsonSchema = JSONSchema.make(schema)
console.log(jsonSchema)
/*
{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: [ 'firstName', 'lastName', 'description' ],
  properties: {
    firstName: { type: 'string', description: 'a string', title: 'First name' },
    lastName: { type: 'string' },
    description: { type: 'string' }
  },
  additionalProperties: false
}
*/

The reason is that JSONSchema.make(schema) targets the encoded part of schema (and the encoded part of Schema.Trim.pipe(Schema.nonEmpty()) is Schema.String, so you get { type: 'string' } as output).

Given your example, it seems you mainly want to set the titles and descriptions for the fields of the struct rather than their types. Therefore, I recommend adding the annotation to the field (via the propertySignature constructor):

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

const schema = Schema.Struct({
  firstName: Schema.propertySignature(Schema.String).annotations({
    title: "First name"
  }),
  lastName: Schema.propertySignature(Schema.Trim.pipe(Schema.nonEmpty())).annotations({
    title: "Last Name"
  }),
  description: Schema.propertySignature(Schema.Trim.pipe(Schema.nonEmpty())).annotations({
    title: "Description"
  })
})

const jsonSchema = JSONSchema.make(schema)
console.log(jsonSchema)
/*
{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: [ 'firstName', 'lastName', 'description' ],
  properties: {
    firstName: { type: 'string', title: 'First name' },
    lastName: { type: 'string', title: 'Last Name' },
    description: { type: 'string', title: 'Description' }
  },
  additionalProperties: false
}
*/

This approach not only works around the current bug but is also more semantically correct ("First name" is the title of the firstName property signature, not its type).

Regarding 2. the problem is that Schema.NullishOr(Schema.Number) translates to undefined | null | number. While null and number can be represented in JSON Schema, undefined cannot, leading the compiler to raise an error. If I understand correctly, based on the expected schema you provided, you are suggesting representing undefined by making the field optional (required: []).

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": [],
  "properties": {
    "numberOfElements": {
      "anyOf": [
        {
          "type": "number"
        },
        {
          "$ref": "#/$defs/null"
        }
      ]
    }
  },
  "additionalProperties": false,
  "$defs": {
    "null": {
      "const": null
    }
  }
}

This approach might indeed be a better default than erroring out. Is this correct?

However, please note that the output won't be { type: 'number' } as you provided, but rather:

{
  "anyOf": [
    {
      "type": "number"
    },
    {
      "$ref": "#/$defs/null"
    }
  ]
}

This is because the compiler still needs to handle the null | number union.

Finally, the following code:

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

const schema = Schema.Struct({
  startDate: Schema.DateFromSelf
})

const jsonSchema = JSONSchema.make(schema)
console.log(jsonSchema)

raises an error because Schema.DateFromSelf represents a Date object, which cannot be represented in JSON Schema.

@mignotju
Copy link
Author

mignotju commented Jul 18, 2024

Thanks a lot for your quick answer!

  1. Thanks for pointing out: Schema.propertySignature. Indeed, it works around the current bug, except it doesn't show the 'type' in the property, that I would also need.

About Schema.NullishOr:

This approach might indeed be a better default than erroring out. Is this correct?

Yes, exactly.

About Schema.DateFromSelf, ok, too bad, that might block me in what I intended to do. Thanks for clarifying.

@mignotju
Copy link
Author

I mean that in the result you just gave, the 'type' is set in firstName, but not in lastName, neither decription.
I can see that you did some fixes, and that we would now get:

{
  '$schema': 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  required: [ 'firstName', 'lastName', 'description' ],
  properties: {
    firstName: { type: 'string', title: 'First name' },
    lastName: { type: 'string', title: 'Last Name' },
    description: { type: 'string', title: 'Description' }
  },
  additionalProperties: false
}

Which is better 👍
Ideally, I would love having: type + title + minLength, but according to what I understood from what you said here:

The reason is that JSONSchema.make(schema) targets the encoded part of schema (and the encoded part of Schema.Trim.pipe(Schema.nonEmpty()) is Schema.String, so you get { type: 'string' } as output).

it is not the expected behaviour.

@gcanti
Copy link
Contributor

gcanti commented Jul 18, 2024

it works around the current bug, except it doesn't show the 'type' in the property, that I would also need

Oh, you're right, sorry. I saw the "type" property in the output correctly because I was testing with the development branch where I'm working on the bug fix.

I'm going to release the patch so you can test it with the updated code.

@gcanti
Copy link
Contributor

gcanti commented Jul 18, 2024

Patch released.

Ideally, I would love having: type + title + minLength, but according to what I understood from what you said here ... it is not the expected behaviour

Exactly, the Schema.nonEmpty() filter is applied after the Trim transformation, so it is ignored.

@mignotju
Copy link
Author

Thanks!

@gcanti
Copy link
Contributor

gcanti commented Jul 18, 2024

Closing for now. Feel free to reopen if any further issues arise.

@gcanti gcanti closed this as completed Jul 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working schema: jsonschema schema
Projects
None yet
Development

No branches or pull requests

2 participants