ts-json-schema-generator icon indicating copy to clipboard operation
ts-json-schema-generator copied to clipboard

Option to generate `if-then-else` schema for union types

Open mdesousa opened this issue 2 years ago • 5 comments

Today (v1.0.0) a union type like the following:

export type Fish = {
  animal_type: 'fish';
  found_in: 'ocean' | 'river';
};

export type Bird = {
  animal_type: 'bird';
  can_fly: boolean;
};

export type Animal = Bird | Fish;

Generates the following json schema:

    "Animal": {
      "anyOf": [
        {
          "$ref": "#/definitions/Bird"
        },
        {
          "$ref": "#/definitions/Fish"
        }
      ]
    },

This fulfills the need to validate the schema. However, when there is an error schema validators like ajv produce errors for all schema paths. As an example, { animal_type: 'fish', found_in: 'lake' } results in the following error:

must have required property 'can_fly': can_fly [keyword: required, instancePath: , schemaPath: #/definitions/Bird/required]
must be equal to one of the allowed values: ocean,river [keyword: enum, instancePath: /found_in, schemaPath: #/definitions/Fish/properties/found_in/enum]
must match a schema in anyOf:  [keyword: anyOf, instancePath: , schemaPath: #/anyOf]

This first line in this error can be misleading since animal_type is fish, and can_fly is not a property of fish. The second line is the real error that we'd like to present. The errors can get more confusing as you add more types to the union type... a lot of errors are shown for types that are irrelevant given the provide animal_type.

A solution to this problem would be to have the option to generate if-then-else json schemas. These provide more information to the validators to pick the appropriate schema. So if we generate the following:

    "Animal": {
        "type": "object",
        "properties": {
          "animal_type": {
            "type": "string",
            "enum": ["fish", "bird"]
          }
        },
    "allOf": [
        {
          "if": {
            "properties": { "animal_type": { "const": "fish" } }
          },
          "then": {
            "$ref": "#/definitions/Fish"
          }
        },
        {
          "if": {
            "properties": { "animal_type": { "const": "bird" } }
          },
          "then": {
            "$ref": "#/definitions/Bird"
          }
        }
      ]
    }

The same input results in the following, more concise error:

must be equal to one of the allowed values: ocean,river [keyword: enum, instancePath: /found_in, schemaPath: #/definitions/Fish/properties/found_in/enum]

mdesousa avatar Apr 05 '22 13:04 mdesousa

@mdesousa,

Excellent description.

I would push back on the validators. The validators have all the same information that this tool does. It is nearly impossible to know which field / fields to use as the type pivot. It gets even more complicated when you add sub-types.

The validator should be able to do a "best-fit" and give the appropriate error.

Jason3S avatar Apr 06 '22 06:04 Jason3S

Yes @Jason3S , you are right... validators do have the same information. However, validators actually did their part by supporting if-then-else and would say "Instead of using an anyOf json-schema, you should be using if-then-else". It's a more clear representation of what is being modeled, as shown in the example documented here.

I think one option to figure this out from TypeScript could be to have an annotation for a property that fulfills the role of kind for a given type. For example:

/**
 * @kind-prop animal_type
 */
export type Animal = Bird | Fish;

This could be used as a hint to generate the schema using if-then-else instead of anyOf, and would provide the name of the property to use in the if conditions. It could also serve to make this an opt-in feature... if not provided, the anyOf schema can be generated.

mdesousa avatar Apr 06 '22 13:04 mdesousa

While it might be easier for validators, we have to consider other common use cases like generating types/classes as in http://github.com/altair-viz/altair, which is generated from the Vega-Lite schema. Since Vega-Lite is the main use case of this library, we need to be careful to support this use case well.

domoritz avatar Apr 06 '22 13:04 domoritz

Definitely wouldn't propose to alter the generation in ways that could break existing implementations. Using an opt-in annotation like the one proposed would mean that everything is generated the same way, unless the if-then-else schema is the desired output.

mdesousa avatar Apr 06 '22 14:04 mdesousa

I tried to avoid having too many knobs so far and would only be willing to accept this change with comprehensive unit tests.

domoritz avatar Apr 06 '22 14:04 domoritz

:rocket: Issue was released in v1.1.0 :rocket:

github-actions[bot] avatar Sep 21 '22 04:09 github-actions[bot]