-
-
Notifications
You must be signed in to change notification settings - Fork 539
Inheritance
NJsonSchema supports inheritance for JSON Schema/Swagger/OpenAPI 3 and C#/TypeScript code generation. For inheritance to work, the serialized JSON object must contain a discriminator field which identifies the actual subclass: The base class needs to add this field during serialization and select the correct type during deserialization. The generated JSON Schema must specify this discriminator field so that the correct deserialization logic can be generated.
The following implementations are supported:
- JsonDerivedTypeAttribute from System.Text.Json
- Custom System.Text.Json converter JsonInheritanceConverter
- Custom Newtonsoft.Json converter JsonInheritanceConverter (sample below)
You can also inherit from the JsonInheritanceConverter
classes and customize the methods.
In your source C# base classes where the schema is generated from and which are used for serialization, you need to add the JsonInheritanceConverter (from the NJsonSchema package or implemented in your own code) and KnownTypeAttribute
s:
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; }
}
Notes:
- Because of an implementation detail, you must define a
public
method when using theKnownType(String)
overload. - It is recommended to NOT implement the discriminator property in your DTO class; if you implement it anyway, you can use it to retrieve the discriminator for deserialized objects, but it will be ignored when serializing the objects (see PR #718)
- Since the handling of inheritance differ between JSON Schema and Swagger/OpenAPI 3, some
allOf
/oneOf
construct may not validate properly in pure JSON Schema validator, this is by-design (see #13 and #302) - If the JsonConverter doesn't work it may be because you may have forgot to add .AddNewtonsoftJson() method at the end of the AddControllersWithViews() from Microsoft.AspNetCore.Mvc.NewtonsoftJson package
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",
"title": "Container",
"additionalProperties": false,
"properties": {
"Animal": {
"oneOf": [
{
"$ref": "#/definitions/Animal"
},
{
"type": "null"
}
]
}
},
"definitions": {
"Dog": {
"type": "object",
"additionalProperties": false,
"properties": {
"Bar": {
"type": [
"null",
"string"
]
}
},
"allOf": [
{
"$ref": "#/definitions/Animal"
}
]
},
"Animal": {
"type": "object",
"discriminator": "discriminator",
"additionalProperties": false,
"required": [
"discriminator"
],
"properties": {
"Foo": {
"type": [
"null",
"string"
]
},
"discriminator": {
"type": "string"
}
}
}
}
}
If you need to change the discriminator values for your type (i.e. they should differ from the actual type names), use an own implementation of the JsonInheritanceConverter
:
public class CustomJsonInheritanceConverter : JsonInheritanceConverter
{
public CustomJsonInheritanceConverter(string name) : base(name)
{
}
public override string GetDiscriminatorValue(Type type)
{
return type.Name.Replace("Dto", string.Empty);
}
protected override Type GetDiscriminatorType(JObject jObject, Type objectType, string discriminatorValue)
{
return base.GetDiscriminatorType(jObject, objectType, discriminatorValue + "Dto");
}
}
[JsonConverter(typeof(CustomJsonInheritanceConverter), "discriminator")]
[KnownType(typeof(Dog))]
public class Animal
{
...
Important: In Swagger 2 the schema name and discriminator values must match because the spec cannot express different names. This has been fixed in OpenAPI 3 with a new inheritance description model.
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);
}
...
}
export class Animal {
foo: string;
private discriminator: string;
constructor(data?: any) {
this.discriminator = "Animal";
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 (data["discriminator"] === "Dog")
return new Dog(data);
return new Animal(data);
}
...
}
export class Dog extends Animal {
bar: string;
constructor(data?: any) {
super(data);
this.discriminator = "Dog";
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"] + "'.");
}
...
}
The generated C# code looks like the original code (the JsonInheritanceConverter
is also generated to avoid a dependency to NJsonSchema):
[JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
[JsonInheritance("Dog", typeof(Dog))]
public partial class Animal
{
[JsonProperty("Foo", Required = Required.Default)]
public string Foo { get; set; }
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
public static Animal FromJson(string data)
{
return JsonConvert.DeserializeObject<Animal>(data);
}
}
public partial class Dog : Animal
{
[JsonProperty("Bar", Required = Required.Default)]
public string Bar { get; set; }
public string ToJson()
{
return JsonConvert.SerializeObject(this);
}
public static Dog FromJson(string data)
{
return JsonConvert.DeserializeObject<Dog>(data);
}
}
internal class JsonInheritanceConverter : JsonConverter
{
...
}
TODO: Document advanced usage and new features: https://github.com/RSuter/NJsonSchema/commit/4b2c224585360b3b2c03143870f001b1c23395b6
To avoid generating a new schema for an base class and instead merge the inherited properties into the class, use the JsonSchemaFlattenAttribute
on a class or set the global FlattenInheritanceHierarchy
setting to true
.