Skip to content
Rico Suter edited this page Jun 24, 2016 · 30 revisions

NJsonSchema supports inheritance for JSON Schema and C#/TypeScript code generation. For inheritance to work, the serialized JSON object must contain a discriminator field which identifies the actual subclass. To support this, your base class needs to add this field during serialization and select the correct type during deserialization. Also, the generated schema must specify this discriminator field so that the correct deserialization logic can be generated. In your source C# base classes where the schema is generated from and which are used for serialization, you need to add the JsonInheritanceConverter and KnownTypeAttributes:

public class Container
{
    public Animal Animal { get; set; }
}

[JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
[KnownType(typeof(Dog))]
public class Animal
{
    public string Foo { get; set; }
}

public class Dog : Animal
{
    public string Bar { get; set; }
}

An instance of Container is then serialized to the following JSON:

{
  "Animal": {
    "discriminator": "Dog",
    "Bar": "bar",
    "Foo": "foo"
  }
}

And the generated JSON Schema for the Container looks like:

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "x-typeName": "Container",
  "additionalProperties": false,
  "properties": {
    "Animal": {
      "oneOf": [
        {
          "$ref": "#/definitions/Animal"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "definitions": {
    "Dog": {
      "type": "object",
      "x-typeName": "Dog",
      "additionalProperties": false,
      "properties": {
        "Bar": {
          "type": [
            "null",
            "string"
          ]
        }
      },
      "allOf": [
        {
          "$ref": "#/definitions/Animal"
        }
      ]
    },
    "Animal": {
      "type": "object",
      "x-typeName": "Animal",
      "discriminator": "discriminator",
      "additionalProperties": false,
      "required": [
        "discriminator"
      ],
      "properties": {
        "Foo": {
          "type": [
            "null",
            "string"
          ]
        },
        "discriminator": {
          "type": "string"
        }
      }
    }
  }
}

From this JSON Schema you can now generate C# or TypeScript code which correctly processes the discriminator field.

For TypeScript you need to use the TypeStyle Class or KnockoutClass so that the deserialization logic is generated:

export class Container { 
    animal: Animal;

    constructor(data?: any) {
        if (data !== undefined) {
            this.animal = data["Animal"] ? Animal.fromJS(data["Animal"]) : null;
        }
    }

    static fromJS(data: any): Container {
        return new Container(data);
    }

    toJS(data?: any) {
        data = data === undefined ? {} : data;
        data["Animal"] = this.animal ? this.animal.toJS() : null;
        return data; 
    }

    toJSON() {
        return JSON.stringify(this.toJS());
    }

    clone() {
        var json = this.toJSON();
        return new Container(JSON.parse(json));
    }
}

export class Animal { 
    foo: string; 
    private discriminator: string;

    constructor(data?: any) {
        if (data !== undefined) {
            this.foo = data["Foo"] !== undefined ? data["Foo"] : null;
            this.discriminator = data["discriminator"] !== undefined ? data["discriminator"] : null;
        }
    }

    static fromJS(data: any): Animal {
        if (/^[$A-Z_][0-9A-Z_$]*$/i.test(data["discriminator"]))
            return <any>eval("new " + data["discriminator"] + "(data)");
        else
            throw new Error("Invalid discriminator '" + data["discriminator"] + "'.");
    }

    toJS(data?: any) {
        data = data === undefined ? {} : data;
        data["Foo"] = this.foo !== undefined ? this.foo : null;
        data["discriminator"] = this.discriminator !== undefined ? this.discriminator : null;
        return data; 
    }

    toJSON() {
        return JSON.stringify(this.toJS());
    }

    clone() {
        var json = this.toJSON();
        return new Animal(JSON.parse(json));
    }
}

export class Dog extends Animal { 
    bar: string;

    constructor(data?: any) {
        super(data);
        if (data !== undefined) {
            this.bar = data["Bar"] !== undefined ? data["Bar"] : null;
        }
    }

    static fromJS(data: any): Dog {
        if (/^[$A-Z_][0-9A-Z_$]*$/i.test(data["discriminator"]))
            return <any>eval("new " + data["discriminator"] + "(data)");
        else
            throw new Error("Invalid discriminator '" + data["discriminator"] + "'.");
    }

    toJS(data?: any) {
        data = data === undefined ? {} : data;
        data["Bar"] = this.bar !== undefined ? this.bar : null;
        super.toJS(data);
        return data; 
    }

    toJSON() {
        return JSON.stringify(this.toJS());
    }

    clone() {
        var json = this.toJSON();
        return new Dog(JSON.parse(json));
    }
}

TODO

Clone this wiki locally