Unexpected result for TypeScript Interfaces using Quicktype 'allOf'

164 Views Asked by At

We are changing from JSON Type Definition to JSON Schema and are now using Quicktype to convert the JSON Schemas to TypeScript Types. For the most part, Quicktype is doing its job very well, but aparently it can't convert Discriminators, or the JSON Schema equivalent allOf with if-then. It seems to just ignore the allOf... Did we do anything wrong?

The bash command we use to convert JSON Schema to TypeScript:

quicktype -o ./src/typings.ts --just-types --acronym-style camel --src-lang schema

Expected Result

export type CreateCustomerCommandPayloadV1 = CreateCustomerCommandPayloadV1Person | CreateCustomerCommandPayloadV1Company;

export interface CreateCustomerCommandPayloadV1Person {
    type: CreateCustomerCommandPayloadV1Type.Person;
    customerKey: string
    firstName: string
    lastName: string
}
export interface CreateCustomerCommandPayloadV1Company {
  type: CreateCustomerCommandPayloadV1Type.Company;
  customerKey: string
  companyName: string
}

export enum CreateCustomerCommandPayloadV1Type {
    Company = "COMPANY",
    Person = "PERSON",
}

Actual Result

export interface CreateCustomerCommandPayloadV1 {
    type: CreateCustomerCommandPayloadV1Type;
}

export enum CreateCustomerCommandPayloadV1Type {
    Company = "COMPANY",
    Person = "PERSON",
}

Our JSON Schema File:

{
  "$schema": "http://json-schema.org/draft-07/schema",
  "metadata": {
    "description": "Command: Creates a new customer (Types: COMPANY | PERSON)",
    "subject": "commands.crm.customers.createCustomer",
    "authenticationRequired": true
  },
  "type": "object",
  "additionalProperties": false,
  "$id": "CreateCustomerCommandPayloadV1",
  "properties": {
    "type": {
      "type": "string",
      "enum": [
        "COMPANY",
        "PERSON"
      ]
    },
    "customerKey": {
      "type": "string"
    }
  },
  "required": [
    "type"
  ],
  "$comment": "discriminator",
  "allOf": [
    {
      "if": {
        "properties": {
          "type": {
            "const": "COMPANY"
          }
        }
      },
      "then": {
        "properties": {
          "companyName": {
            "type": "string"
          }
        },
        "required": [
          "companyName"
        ]
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "PERSON"
          }
        }
      },
      "then": {
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
        },
        "type": "object",
        "additionalProperties": false,
        "title": "CreateCustomerCommandPayloadV1Person",
        "required": [
          "firstName",
          "lastName"
        ]
      }
    }
  ]
}
2

There are 2 best solutions below

0
Jeremy Fiel On BEST ANSWER

If you want to change your schema to use oneOf rather than if, then...these are the results. Still leaves a lot to be desired.

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "metadata": {
        "description": "Command: Creates a new customer (Types: COMPANY | PERSON)",
        "subject": "commands.crm.customers.createCustomer",
        "authenticationRequired": true
    },
    "type": "object",
    "$id": "CreateCustomerCommandPayloadV1",
    "oneOf": [
        {
            "type": "object",
            "properties": {
                "type": {
                    "type": "string",
                    "enum": [
                        "PERSON"
                    ]
                },
                "customerKey": {
                    "type": "string"
                },
                "firstName": {
                    "type": "string"
                },
                "lastName": {
                    "type": "string"
                }
            },
            "additionalProperties": false,
            "required": [
                "type",
                "firstName",
                "lastName"
            ]
        },
        {
            "type": "object",
            "properties": {
                "type": {
                    "type": "string",
                    "enum": [
                        "COMPANY"
                    ]
                },
                "companyName": {
                    "type": "string"
                },
                "customerKey": {
                    "type": "string"
                }
            },
            "additionalProperties": false,
            "required": [
                "type",
                "companyName"
            ]
        }
    ]
}
export interface Typings {
    customerKey?: string;
    firstName?:   string;
    lastName?:    string;
    type:         Type;
    companyName?: string;
}

export enum Type {
    Company = "COMPANY",
    Person = "PERSON",
}

quicktype --out typings.ts --just-types  --acronym-style camel --src-lang schema --src schema.json --lang ts
0
Jeremy Fiel On

This is where additionalProperties: false and allOf cause a lot of confusion and heartache.

Prior to 2019-09, this combination of keywords conflicts with how many people believe it should work. In reality, it validates each schema separately: the root, and each allOf subschema. When you add additionalProperties: false to the root schema, it means no other properties are allowed, regardless of how many subschemas define other properties.

take this example: Every possible schema will fail for additionalProperties except the following instances.

{}
true
false
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "additionalProperties": false,
  "allOf": [
    {"properties": {"name": {"type": "string"}}},
    {"properties": {"age": {"type": "number"}}}
  ]
}

In order to fix your issue from a JSON Schema perspective, you need to modify the root schema to include the additionalProperties you would expect from the allOf subschemas. Giving each of them a true boolean schema is sufficient for the validator to recognize these properties are allowed.

You will also notice in the if, then, the type property must be defined again with a true boolean schema to be recognized as a valid property because you have set additionalProperties: false once more in the then statement.

Fixing these issues in your schema doesn't guarantee QuickType will support this schema. You may need to further follow up with their project maintainers.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "metadata": {
    "description": "Command: Creates a new customer (Types: COMPANY | PERSON)",
    "subject": "commands.crm.customers.createCustomer",
    "authenticationRequired": true
  },
  "type": "object",
  "additionalProperties": false,
  "$id": "CreateCustomerCommandPayloadV1",
  "properties": {
    "type": {
      "type": "string",
      "enum": [
        "COMPANY",
        "PERSON"
      ]
    },
    "customerKey": {
      "type": "string"
    },
    "companyName": true,
    "firstName": true,
    "lastName": true
  },
  "required": [
    "type"
  ],
  "$comment": "discriminator",
  "allOf": [
    {
      "if": {
        "properties": {
          "type": {
            "const": "COMPANY"
          }
        }
      },
      "then": {
        "properties": {
          "companyName": {
            "type": "string"
          }
        },
        "required": [
          "companyName"
        ]
      }
    },
    {
      "if": {
        "properties": {
          "type": {
            "const": "PERSON"
          }
        }
      },
      "then": {
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "type": true
        },
        "type": "object",
        "additionalProperties": false,
        "title": "CreateCustomerCommandPayloadV1Person",
        "required": [
          "firstName",
          "lastName"
        ]
      }
    }
  ]
}

I'm pretty sure this is not your expected results, but this is what QuickType came up with after modifying this schema.

export interface Typings {
    companyName?: any;
    customerKey?: string;
    firstName?:   any;
    lastName?:    any;
    type:         Type;
}

export enum Type {
    Company = "COMPANY",
    Person = "PERSON",
}


 quicktype -o typings.ts --just-types --acronym-style camel -s schema schema.json