Effect-TS / schema

Modeling the schema of data structures as first-class values

Home Page:https://effect.website

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JSON Schema: literal should be converted to enum instead of anyOf

huypham50 opened this issue · comments

commented

What is the problem this feature would solve?

Coming from pydantic, literals are converted to enums in json schema:

import json
from enum import Enum
from typing import Literal

from pydantic import BaseModel

class Gender(str, Enum):
    male = 'male'
    female = 'female'
    other = 'other'
    not_given = 'not_given'

class Schema(BaseModel):
    degree: Literal['bachelor', 'master', 'doctor']
    gender: Gender

print(json.dumps(Schema.model_json_schema(), indent=2))

{
  "$defs": {
    "Gender": {
      "enum": [
        "male",
        "female",
        "other",
        "not_given"
      ],
      "title": "Gender",
      "type": "string"
    }
  },
  "properties": {
    "degree": {
      "enum": [
        "bachelor",
        "master",
        "doctor"
      ],
      "title": "Degree",
      "type": "string"
    },
    "gender": {
      "$ref": "#/$defs/Gender"
    }
  },
  "required": [
    "degree",
    "gender"
  ],
  "title": "Schema",
  "type": "object"
}

However, in effect, literals will be converted to anyOf

const degreeSchema = S.literal('bachelor', 'master', 'doctor')
const jsonSchema = JSONSchema.to(degreeSchema);

"gender": {
  "anyOf": [
    {
      "type": "string",
      "const": "bachelor"
    },
    {
      "type": "string",
      "const": "master"
    },
    {
      "type": "string",
      "const": "doctor"
    }
  ],
}

Would slightly prefer enums in this case because it's stricter than anyOf (literals have strict nature imo).

What is the feature you are proposing to solve the problem?

Literals should be converted to enums instead of anyof

What alternatives have you considered?

Current workaround is using enums (not recommended) or records (too verbose)

  const degree = S.enums({
    HighSchool: 'HighSchool',
    University: 'University',
    Graduate: 'Graduate',
  } as const)

Problem with enums is that AFAIK there's no room for meta data such as title, description, etc..., we could use oneOf + const:

"gender": {
  "oneOf": [
    {
      "const": "male",
      "description": "..."
    },
    {
      "const": "female"
    }
  ],
}
commented

Maybe something like this?

const enumJsonSchema = S.literal('male', 'female')
const anyOfJsonSchema = S.union(S.literal('male'), S.literal('female'))

Yeah, however S.literal('male', 'female') is just a shorthand for S.union(S.literal('male'), S.literal('female')), they yield the same schema.

So I guess we must identify the members of a union that are literals without any meta data, group them toghether and generate for them a JSON Schema in the form of { enum: [...] }.

Like:

const schema = S.union(
  S.literal(1, 2),
  S.literal(true).pipe(S.description("description")),
  S.string
)

const jsonSchema = JSONSchema.to(schema)

expect(jsonSchema).toEqual({
  "$schema": "http://json-schema.org/draft-07/schema#",
  "anyOf": [
    { "const": true, "description": "description" },
    {
      "type": "string",
      "description": "a string",
      "title": "string"
    },
    { "enum": [1, 2] }
  ]
})

And then S.literal(...) is just a special case of the above

const schema = S.literal(1, 2)

const jsonSchema = JSONSchema.to(schema)

expect(jsonSchema).toEqual({
  "$schema": "http://json-schema.org/draft-07/schema#",
  "enum": [1, 2]
})