vega / ts-json-schema-generator

Generate JSON schema from your Typescript sources
MIT License
1.45k stars 192 forks source link

Way to define `not` in combination with `discriminator` #2046

Open GKersten opened 2 months ago

GKersten commented 2 months ago

Trying to make my way around using this library, so if I missed something please let me know.

I have taken some inspiration from this test: https://github.com/vega/ts-json-schema-generator/blob/next/test/valid-data/discriminator/main.ts

I want to extend it, so that my root JSON also allows for any other user defined JSON, but as soon as a "type" is defined it needs to be strict and validate with a definition using the discriminator.

So some examples:

// should fail: dog is not an "animal_type"
{
  "animal_type": "dog",
}

// should fail: bird is missing "animal_type"
{
  "animal_type": "can_fly",
}

// should succeed
{
  "animal_type": "bird",
  "can_fly": true
}

// should succeed: "animal_type" is not specified, so allow any object
{
  "id": 123,
}

This is what I tried:

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

export interface Bird {
  animal_type: 'bird';
  can_fly: boolean;
}

/**
 * @discriminator animal_type
 */
export type Animal = Bird | Fish;

export type Root = Animal | any;

Output:

{
  "$ref": "#/definitions/Root",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "Animal": {
      "allOf": [
        {
          "if": {
            "properties": {
              "animal_type": {
                "const": "bird",
                "type": "string"
              }
            }
          },
          "then": {
            "$ref": "#/definitions/Bird"
          }
        },
        {
          "if": {
            "properties": {
              "animal_type": {
                "const": "fish",
                "type": "string"
              }
            }
          },
          "then": {
            "$ref": "#/definitions/Fish"
          }
        }
      ],
      "properties": {
        "animal_type": {
          "enum": [
            "bird",
            "fish"
          ]
        }
      },
      "required": [
        "animal_type"
      ],
      "type": "object"
    },
    "Bird": {
      "additionalProperties": false,
      "properties": {
        "animal_type": {
          "const": "bird",
          "type": "string"
        },
        "can_fly": {
          "type": "boolean"
        }
      },
      "required": [
        "animal_type",
        "can_fly"
      ],
      "type": "object"
    },
    "Fish": {
      "additionalProperties": false,
      "properties": {
        "animal_type": {
          "const": "fish",
          "type": "string"
        },
        "found_in": {
          "enum": [
            "ocean",
            "river"
          ],
          "type": "string"
        }
      },
      "required": [
        "animal_type",
        "found_in"
      ],
      "type": "object"
    },
    "Root": {
      "anyOf": [
        {
          "$ref": "#/definitions/Animal"
        },
        {}
      ]
    }
  }
}

Now with this output schema, actually everything becomes valid, because there is always a fallback to any.


What I think I need to add is a not keyword. To make sure the any type does not include animal_type. See: https://json-schema.org/understanding-json-schema/reference/combining#not

Adjusting the above output to the following, seems to result in the behaviour I am looking for:

...
"Root": {
  "anyOf": [
    {
      "$ref": "#/definitions/Animal"
    },
    {
      "not": {
        "required": ["animal_type"]
      }
    }
  ]
}
...

Is there any way to achieve this currently? I still have to figure out if the custom formatting / parsing is a way to achieve this?


Alternatively I could suggest something like this could result in the desired output, but will probably require more research:

/**
 * @not animal_type
 */
export type AnyObject = Record<string, any>;

export type Root = Animal | AnyObject;

btw, thanks for the great work on the library, it looks really promising. Still checking if it will support some more complex scenarios we might run into. Using this tool will be great because it will take away the pain to maintain a really large JSON Schema manually!

GKersten commented 2 months ago

For now, was able to work-around it with the following config:


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

export interface Bird {
  animal_type: 'bird';
  can_fly: boolean;
}

export interface AnyObject {
  animal_type: 'user_data';
  [key: string]: any;
}

/**
 * @discriminator animal_type
 */
export type Root = Bird | Fish | AnyObject;

But this now requires me to always add a animal_type property, and set it to user_data to allow any other json. Would still like to know if there is a better alternative.