bcherny/json-schema-to-typescript

Cyclical dependency in oneOf causes generation to fatal

sey opened this issue · 1 comments

sey commented

I am trying to define a JSON schema for expressions (similar to what Mongo does).

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "$ref": "#/definitions/expression"
  },
  "definitions": {
    "fieldExpression": {
      "type": "object",
      "propertyNames": { "type": "string" },
      "patternProperties": {
        "^.*$": {
          "type": "object",
          "properties": {
            "$eq": { "type": ["boolean", "number", "string", "null"] },
            "$ne": { "type": ["boolean", "number", "string", "null"] },
            "$gt": { "type": ["number", "string"] },
            "$gte": { "type": ["number", "string"] },
            "$lt": { "type": ["number", "string"] },
            "$lte": { "type": ["number", "string"] }
          },
          "minProperties": 1,
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "logicalOperator": {
      "type": "object",
      "oneOf": [
        {
          "properties": {
            "$and": {
              "type": "array",
              "items": { "$ref": "#/definitions/expression" },
              "minItems": 1
            }
          },
          "required": ["$and"],
          "additionalProperties": false
        },
        {
          "properties": {
            "$or": {
              "type": "array",
              "items": { "$ref": "#/definitions/expression" },
              "minItems": 1
            }
          },
          "required": ["$or"],
          "additionalProperties": false
        },
        {
          "properties": {
            "$not": { "$ref": "#/definitions/expression" }
          },
          "required": ["$not"],
          "additionalProperties": false
        }
      ]
    },
    "expression": {
      "oneOf": [
        { "$ref": "#/definitions/fieldExpression" },
        { "$ref": "#/definitions/logicalOperator" }
      ]
    }
  }
}

It fails with

[
  TypeError: Converting circular structure to JSON
      --> starting at object with constructor 'Array'
      |     index 1 -> object with constructor 'Object'
      |     property 'oneOf' -> object with constructor 'Array'
      |     ...
      |     index 0 -> object with constructor 'Object'
      --- property 'oneOf' closes the circle
      at JSON.stringify (<anonymous>)
      at generateRawType (/Users/florian/Developer/repositories/proximus/digital-kyc-onboarding-app/node_modules/json-schema-to-typescript/dist/src/generator.js:171:25)

This is due to logical operator being recursive.

Are you aware of a way to express the recursive aspect in a different way supported by this package?

I think your schema is wrong. This:

  "properties": {
    "$ref": "#/definitions/expression"
  },

translates to:

  "properties": {
      "oneOf": [
        { "$ref": "#/definitions/fieldExpression" },
        { "$ref": "#/definitions/logicalOperator" }
      ]
  },

which I think doesn't make sense. I tried three online JSON schema validators and they agreed. So I think this is a +1 for #48.

This works:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "title": "Expression",
  "oneOf": [
    {
      "$ref": "#/definitions/fieldExpression"
    },
    {
      "$ref": "#/definitions/logicalOperator"
    }
  ],
  "definitions": {
    "fieldExpression": {
      "type": "object",
      "propertyNames": {
        "type": "string"
      },
      "patternProperties": {
        "^.*$": {
          "type": "object",
          "properties": {
            "$eq": {
              "type": [
                "boolean",
                "number",
                "string",
                "null"
              ]
            },
            "$ne": {
              "type": [
                "boolean",
                "number",
                "string",
                "null"
              ]
            },
            "$gt": {
              "type": [
                "number",
                "string"
              ]
            },
            "$gte": {
              "type": [
                "number",
                "string"
              ]
            },
            "$lt": {
              "type": [
                "number",
                "string"
              ]
            },
            "$lte": {
              "type": [
                "number",
                "string"
              ]
            }
          },
          "minProperties": 1,
          "additionalProperties": false
        }
      },
      "additionalProperties": false
    },
    "logicalOperator": {
      "type": "object",
      "oneOf": [
        {
          "properties": {
            "$and": {
              "type": "array",
              "items": {
                "$ref": "#"
              },
              "minItems": 1
            }
          },
          "required": [
            "$and"
          ],
          "additionalProperties": false
        },
        {
          "properties": {
            "$or": {
              "type": "array",
              "items": {
                "$ref": "#"
              },
              "minItems": 1
            }
          },
          "required": [
            "$or"
          ],
          "additionalProperties": false
        },
        {
          "properties": {
            "$not": {
              "$ref": "#"
            }
          },
          "required": [
            "$not"
          ],
          "additionalProperties": false
        }
      ]
    }
  }
}

Output:

/* eslint-disable */
/**
 * This file was automatically generated by json-schema-to-typescript.
 * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
 * and run json-schema-to-typescript to regenerate this file.
 */

export type Expression = FieldExpression | LogicalOperator;
export type LogicalOperator =
  | {
      /**
       * @minItems 1
       */
      $and: [Expression, ...Expression[]];
    }
  | {
      /**
       * @minItems 1
       */
      $or: [Expression, ...Expression[]];
    }
  | {
      $not: Expression;
    };

export interface FieldExpression {
  /**
   * This interface was referenced by `FieldExpression`'s JSON-Schema definition
   * via the `patternProperty` "^.*$".
   */
  [k: string]: {
    $eq?: boolean | number | string | null;
    $ne?: boolean | number | string | null;
    $gt?: number | string;
    $gte?: number | string;
    $lt?: number | string;
    $lte?: number | string;
  };
}