json-schema-to-typescript icon indicating copy to clipboard operation
json-schema-to-typescript copied to clipboard

allOf, oneOf dont correctly generate types

Open italicbold opened this issue 3 years ago • 8 comments

The following schema:

        "Test": {
            "type": "object",
            "properties": {
                "prop1": { "type": "string" },
                "prop2": { "type": "string" },
                "prop3": { "type": "string"},
                "prop4": { 
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "minItems": 1
                },
                "prop5": { "type": "string" },
                "prop6": { "type": "string" }
            },
            "allOf": [
                { "required": [ "prop1" ] },
                {
                    "oneOf": [
                        { "required": [ "prop2" ] },
                        { "required": [ "prop3" ] },
                        { "required": [ "prop4" ] },
                        { "required": [ "prop5" ] },
                        { "required": [ "prop6" ] }
                    ]
                }
            ],
            "additionalProperties": false
        }

Generates type:

type Test = {
    [k: string]: unknown;
} & (
    | {
          [k: string]: unknown;
      }
    | {
          [k: string]: unknown;
      }
    | {
          [k: string]: unknown;
      }
    | {
          [k: string]: unknown;
      }
    | {
          [k: string]: unknown;
      }
);

Expected type:

type Test = {
    prop1: string;
} & (
    { prop2: string } |
    { prop3: string } |
    { prop4: string[] } |
    { prop5: string } |
    { prop6: string }
);

italicbold avatar Apr 13 '21 10:04 italicbold

Similar to https://github.com/bcherny/json-schema-to-typescript/issues/378 ?

Used version 10.1.4

italicbold avatar Apr 13 '21 10:04 italicbold

@italicbold the mentioned issue is related to the use of allOf, anyOf with properties, and in that case there was an error in the provided example. Here the problem is related to the processing of requested property when inside oneOf, anyOf. I think it is different.

RickZack avatar Apr 15 '21 23:04 RickZack

I'm seeing the same problem. Usage of anyOf causes schema to devolve to [k: string]: unknown;

john-twigg-ck avatar Apr 16 '21 18:04 john-twigg-ck

I found that nesting oneOf/anyOf/allOf causes the issue:

Minimum example schema:

{
  "oneOf": [ // or "allOf" or "anyOf"
    {
      "properties": {
        "speed": { "type": "string" }
      },
      "oneOf": [ // or "allOf" or "anyOf"
        { "required": ["speed"] }
      ]
    }
  ]
}
Expected vs. actual output

Expected output:

export type Demo = {
  speed?: string;
  [k: string]: unknown;
};

Actual output:

export type Demo = {
  [k: string]: unknown;
};
Workaround that yields the expected output
 {
   "oneOf": [ // or "allOf" or "anyOf"
     {
       "properties": {
         "speed": { "type": "string" }
       },
+      "if": true,
+      "then": {
         "oneOf": [ // or "allOf" or "anyOf"
           { "required": ["speed"] }
         ]
+      }
     }
   ]
 }
Example schema with a bit more context
{
  "oneOf": [
    {
      "properties": {
        "type": { "const": "NoFunction" }
      },
      "required": ["type"],
      "additionalProperties": false
    },
    {
      "properties": {
        "type": { "const": "ShutterStrobe" },
        "speed": { "type": "string" },
        "speedStart": { "type": "string" },
        "speedEnd": { "type": "string" }
      },
      "required": ["type"],
      "oneOf": [
        { "required": ["speed"] },
        { "required": ["speedStart"] }
      ],
      "dependencies": {
        "speedStart": ["speedEnd"],
        "speedEnd": ["speedStart"]
      },
      "additionalProperties": false
    }
  ]
}

When removing the inner oneOf/anyOf/allOf, or when not being nested in an outer oneOf/anyOf/allOf, the output is as expected.

FloEdelmann avatar Jun 19 '21 13:06 FloEdelmann

AFAICT, it isn't valid JSON schema to use { required: ["property"] } as an allOf item. JSON schema's docs say:

allOf can not be used to “extend” a schema to add more details to it in the sense of object-oriented inheritance. Instances must independently be valid against “all of” the schemas in the allOf. See the section on Subschema Independence for more information.

(Source)

So there doesn't seem to be a problem with json-schema-to-typescript. As for how to generate the TypeScript interface you want, I think the following code achieves the behavior you're looking for. It's more verbose than you'd like, but IMO it's also easier to understand what's going on in the schema.

JSON Schema

View
{
  "title": "Test",
  "type": "object",
  "allOf": [
    {
      "type": "object",
      "properties": {
        "prop1": { "type": "string" },
        "prop2": { "type": "string" },
        "prop3": { "type": "string" },
        "prop4": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "minItems": 1
        },
        "prop5": { "type": "string" },
        "prop6": { "type": "string" }
      },
      "additionalProperties": false
    },
    {
      "type": "object",
      "properties": { "prop1": { "type": "string" } },
      "required": ["prop1"],
      "additionalProperties": false
    },
    {
      "oneOf": [
        {
          "type": "object",
          "properties": { "prop2": { "type": "string" } },
          "required": ["prop2"],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": { "prop3": { "type": "string" } },
          "required": ["prop3"],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": {
            "prop4": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "minItems": 1
            }
          },
          "required": ["prop4"],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": { "prop5": { "type": "string" } },
          "required": ["prop5"],
          "additionalProperties": false
        },
        {
          "type": "object",
          "properties": { "prop6": { "type": "string" } },
          "required": ["prop6"],
          "additionalProperties": false
        }
      ]
    }
  ]
}

Output

export type Test = {
  prop1?: string;
  prop2?: string;
  prop3?: string;
  prop4?: [string, ...string[]];
  prop5?: string;
  prop6?: string;
} & {
  prop1: string;
} & (
    | {
        prop2: string;
      }
    | {
        prop3: string;
      }
    | {
        prop4: [string, ...string[]];
      }
    | {
        prop5: string;
      }
    | {
        prop6: string;
      }
  );

amh4r avatar Sep 25 '21 18:09 amh4r

Reading this and this, I would argue, that { "required": ["foo"] } is a perfectly valid schema, that means "if the value is an object, it must have a property foo". In terms of TypeScript that would be Not<object> | { foo: unknown } except there is no Not and I know of no way to express such a thing in TS.

openreply-dleinhaeuser avatar Nov 29 '21 09:11 openreply-dleinhaeuser

Any news on this one? Is someone working on it?

ericmorand avatar Aug 03 '22 11:08 ericmorand