Repository: vega/ts-json-schema-generator (TypeScript)

Issue:
discriminator: simpler OpenAPI schema
It looks like `ajv` now supports the OpenAPI [discriminator keyword](https://ajv.js.org/json-schema.html#discriminator), which seems to have better support and is a bit simpler than `if-then-else`.

```
{
  type: "object",
  discriminator: {propertyName: "foo"},
  required: ["foo"],
  oneOf: [
    {
      properties: {
        foo: {const: "x"},
        a: {type: "string"},
      },
      required: ["a"],
    },
    {
      properties: {
        foo: {enum: ["y", "z"]},
        b: {type: "string"},
      },
      required: ["b"],
    },
  ],
}
```

@daanboer @domoritz @mvanniekerkSQ  i know you have been contributing to this feature (thanks for that by the way!)
how do you feel about switching to this approach?


Discussion:
I think we need to wait for others like vscode to catch up and support it before we can use it. This also doesn't seem like an official json schema property, right? I'd be okay with adding it as an additional hint but not to replace existing logic at this point. 
you are right... even though ajv is supporting it, it's not json schema yet... it's in the OpenAPI spec. the additional hint sounds like a good option 👍 
<!-- GITHUB_RELEASE COMMENT: released -->
:rocket: Issue was released in [`v1.3.0-next.0`](https://github.com/vega/ts-json-schema-generator/releases/tag/v1.3.0-next.0) :rocket:



Patch:
diff --git a/factory/formatter.ts b/factory/formatter.ts
index b030885ee..1d095c0b2 100644
--- a/factory/formatter.ts
+++ b/factory/formatter.ts
@@ -75,7 +75,7 @@ export function createFormatter(config: Config, augmentor?: FormatterAugmentor):
 
         .addTypeFormatter(new ArrayTypeFormatter(circularReferenceTypeFormatter))
         .addTypeFormatter(new TupleTypeFormatter(circularReferenceTypeFormatter))
-        .addTypeFormatter(new UnionTypeFormatter(circularReferenceTypeFormatter))
+        .addTypeFormatter(new UnionTypeFormatter(circularReferenceTypeFormatter, config.discriminatorType))
         .addTypeFormatter(new IntersectionTypeFormatter(circularReferenceTypeFormatter));
 
     return circularReferenceTypeFormatter;
diff --git a/src/Config.ts b/src/Config.ts
index a4c449822..29114d1f8 100644
--- a/src/Config.ts
+++ b/src/Config.ts
@@ -13,6 +13,7 @@ export interface Config {
     encodeRefs?: boolean;
     extraTags?: string[];
     additionalProperties?: boolean;
+    discriminatorType?: "json-schema" | "open-api";
 }
 
 export const DEFAULT_CONFIG: Omit<Required<Config>, "path" | "type" | "schemaId" | "tsconfig"> = {
@@ -26,4 +27,5 @@ export const DEFAULT_CONFIG: Omit<Required<Config>, "path" | "type" | "schemaId"
     minify: false,
     extraTags: [],
     additionalProperties: false,
+    discriminatorType: "json-schema",
 };
diff --git a/src/TypeFormatter/UnionTypeFormatter.ts b/src/TypeFormatter/UnionTypeFormatter.ts
index 5f8cd792d..ac932e0ff 100644
--- a/src/TypeFormatter/UnionTypeFormatter.ts
+++ b/src/TypeFormatter/UnionTypeFormatter.ts
@@ -10,70 +10,91 @@ import { derefType } from "../Utils/derefType";
 import { getTypeByKey } from "../Utils/typeKeys";
 import { uniqueArray } from "../Utils/uniqueArray";
 
+type DiscriminatorType = "json-schema" | "open-api";
+
 export class UnionTypeFormatter implements SubTypeFormatter {
-    public constructor(protected childTypeFormatter: TypeFormatter) {}
+    public constructor(protected childTypeFormatter: TypeFormatter, private discriminatorType?: DiscriminatorType) {}
 
     public supportsType(type: UnionType): boolean {
         return type instanceof UnionType;
     }
-    public getDefinition(type: UnionType): Definition {
-        const definitions = type
+    private getTypeDefinitions(type: UnionType) {
+        return type
             .getTypes()
             .filter((item) => !(derefType(item) instanceof NeverType))
             .map((item) => this.childTypeFormatter.getDefinition(item));
-
+    }
+    private getJsonSchemaDiscriminatorDefinition(type: UnionType): Definition {
+        const definitions = this.getTypeDefinitions(type);
         const discriminator = type.getDiscriminator();
-        if (discriminator !== undefined) {
-            const kindTypes = type
-                .getTypes()
-                .filter((item) => !(derefType(item) instanceof NeverType))
-                .map((item) => getTypeByKey(item, new LiteralType(discriminator)));
+        if (!discriminator) throw new Error("discriminator is undefined");
+        const kindTypes = type
+            .getTypes()
+            .filter((item) => !(derefType(item) instanceof NeverType))
+            .map((item) => getTypeByKey(item, new LiteralType(discriminator)));
 
-            const undefinedIndex = kindTypes.findIndex((item) => item === undefined);
+        const undefinedIndex = kindTypes.findIndex((item) => item === undefined);
 
-            if (undefinedIndex != -1) {
-                throw new Error(
-                    `Cannot find discriminator keyword "${discriminator}" in type ${JSON.stringify(
-                        type.getTypes()[undefinedIndex]
-                    )}.`
-                );
-            }
+        if (undefinedIndex != -1) {
+            throw new Error(
+                `Cannot find discriminator keyword "${discriminator}" in type ${JSON.stringify(
+                    type.getTypes()[undefinedIndex]
+                )}.`
+            );
+        }
 
-            const kindDefinitions = kindTypes.map((item) => this.childTypeFormatter.getDefinition(item as BaseType));
+        const kindDefinitions = kindTypes.map((item) => this.childTypeFormatter.getDefinition(item as BaseType));
 
-            const allOf = [];
+        const allOf = [];
 
-            for (let i = 0; i < definitions.length; i++) {
-                allOf.push({
-                    if: {
-                        properties: { [discriminator]: kindDefinitions[i] },
-                    },
-                    then: definitions[i],
-                });
-            }
+        for (let i = 0; i < definitions.length; i++) {
+            allOf.push({
+                if: {
+                    properties: { [discriminator]: kindDefinitions[i] },
+                },
+                then: definitions[i],
+            });
+        }
 
-            const kindValues = kindDefinitions
-                .map((item) => item.const)
-                .filter((item): item is string | number | boolean | null => item !== undefined);
-
-            const duplicates = kindValues.filter((item, index) => kindValues.indexOf(item) !== index);
-            if (duplicates.length > 0) {
-                throw new Error(
-                    `Duplicate discriminator values: ${duplicates.join(", ")} in type ${JSON.stringify(
-                        type.getName()
-                    )}.`
-                );
-            }
+        const kindValues = kindDefinitions
+            .map((item) => item.const)
+            .filter((item): item is string | number | boolean | null => item !== undefined);
 
-            const properties = {
-                [discriminator]: {
-                    enum: kindValues,
-                },
-            };
+        const duplicates = kindValues.filter((item, index) => kindValues.indexOf(item) !== index);
+        if (duplicates.length > 0) {
+            throw new Error(
+                `Duplicate discriminator values: ${duplicates.join(", ")} in type ${JSON.stringify(type.getName())}.`
+            );
+        }
+
+        const properties = {
+            [discriminator]: {
+                enum: kindValues,
+            },
+        };
 
-            return { type: "object", properties, required: [discriminator], allOf };
+        return { type: "object", properties, required: [discriminator], allOf };
+    }
+    private getOpenApiDiscriminatorDefinition(type: UnionType): Definition {
+        const oneOf = this.getTypeDefinitions(type);
+        const discriminator = type.getDiscriminator();
+        if (!discriminator) throw new Error("discriminator is undefined");
+        return {
+            type: "object",
+            discriminator: { propertyName: discriminator },
+            required: [discriminator],
+            oneOf,
+        } as JSONSchema7;
+    }
+    public getDefinition(type: UnionType): Definition {
+        const discriminator = type.getDiscriminator();
+        if (discriminator !== undefined) {
+            if (this.discriminatorType === "open-api") return this.getOpenApiDiscriminatorDefinition(type);
+            return this.getJsonSchemaDiscriminatorDefinition(type);
         }
 
+        const definitions = this.getTypeDefinitions(type);
+
         // TODO: why is this not covered by LiteralUnionTypeFormatter?
         // special case for string literals | string -> string
         let stringType = true;


Test patch:
diff --git a/test/utils.ts b/test/utils.ts
index 04200688f..b54c3e4de 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -12,7 +12,7 @@ import { UnknownTypeError } from "../src/Error/UnknownTypeError";
 import { SchemaGenerator } from "../src/SchemaGenerator";
 import { BaseType } from "../src/Type/BaseType";
 
-const validator = new Ajv();
+const validator = new Ajv({ discriminator: true });
 addFormats(validator);
 
 const basePath = "test/valid-data";
@@ -49,7 +49,8 @@ export function assertValidSchema(
          * @default {strict:false}
          */
         ajvOptions?: AjvOptions;
-    }
+    },
+    discriminatorType?: Config["discriminatorType"]
 ) {
     return (): void => {
         const config: Config = {
@@ -57,6 +58,7 @@ export function assertValidSchema(
             type,
             jsDoc,
             extraTags,
+            discriminatorType,
             skipTypeCheck: !!process.env.FAST_TEST,
         };
 
diff --git a/test/valid-data-annotations.test.ts b/test/valid-data-annotations.test.ts
index ffa9e1ba1..f52e823bd 100644
--- a/test/valid-data-annotations.test.ts
+++ b/test/valid-data-annotations.test.ts
@@ -75,4 +75,9 @@ describe("valid-data-annotations", () => {
     it("annotation-union-if-then", assertValidSchema("annotation-union-if-then", "Animal", "basic"));
 
     it("annotation-nullable-definition", assertValidSchema("annotation-nullable-definition", "MyObject", "extended"));
+
+    it(
+        "discriminator",
+        assertValidSchema("discriminator", "Animal", "basic", undefined, undefined, undefined, "open-api")
+    );
 });
diff --git a/test/valid-data/discriminator/main.ts b/test/valid-data/discriminator/main.ts
new file mode 100644
index 000000000..e46709edd
--- /dev/null
+++ b/test/valid-data/discriminator/main.ts
@@ -0,0 +1,14 @@
+export type Fish = {
+    animal_type: "fish";
+    found_in: "ocean" | "river";
+};
+
+export type Bird = {
+    animal_type: "bird";
+    can_fly: boolean;
+};
+
+/**
+ * @discriminator animal_type
+ */
+export type Animal = Bird | Fish;
diff --git a/test/valid-data/discriminator/schema.json b/test/valid-data/discriminator/schema.json
new file mode 100644
index 000000000..723f58dbf
--- /dev/null
+++ b/test/valid-data/discriminator/schema.json
@@ -0,0 +1,61 @@
+{
+  "$ref": "#/definitions/Animal",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "definitions": {
+    "Animal": {
+      "discriminator": {
+        "propertyName": "animal_type"
+      },
+      "oneOf": [
+        {
+          "$ref": "#/definitions/Bird"
+        },
+        {
+          "$ref": "#/definitions/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"
+    }
+  }
+}
diff --git a/test/valid-data/literal-object-type-with-computed-props/schema.json b/test/valid-data/literal-object-type-with-computed-props/schema.json
index b835841d4..f84123e10 100644
--- a/test/valid-data/literal-object-type-with-computed-props/schema.json
+++ b/test/valid-data/literal-object-type-with-computed-props/schema.json
@@ -1,20 +1,20 @@
 {
-  "$schema": "http://json-schema.org/draft-07/schema#",
   "$ref": "#/definitions/MyType",
+  "$schema": "http://json-schema.org/draft-07/schema#",
   "definitions": {
     "MyType": {
-      "type": "object",
+      "additionalProperties": false,
       "properties": {
-        "localKey": {
+        "exportedEnumKey": {
           "type": "string"
         },
-        "localEnumKey": {
+        "exportedKey": {
           "type": "string"
         },
-        "exportedKey": {
+        "localEnumKey": {
           "type": "string"
         },
-        "exportedEnumKey": {
+        "localKey": {
           "type": "string"
         }
       },
@@ -24,7 +24,7 @@
         "exportedKey",
         "exportedEnumKey"
       ],
-      "additionalProperties": false
+      "type": "object"
     }
   }
 }