JsonSubTypes 2.1.0

dotnet add package JsonSubTypes --version 2.1.0
                    
NuGet\Install-Package JsonSubTypes -Version 2.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JsonSubTypes" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JsonSubTypes" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="JsonSubTypes" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JsonSubTypes --version 2.1.0
                    
#r "nuget: JsonSubTypes, 2.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JsonSubTypes@2.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JsonSubTypes&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=JsonSubTypes&version=2.1.0
                    
Install as a Cake Tool

JsonSubTypes

JsonSubTypes is a discriminated Json sub-type Converter implementation for .NET

CI CodeQL Code Coverage Quality Gate Status NuGet NuGet CodeFactor FOSSA Status

Note: this library is built around Json.NET/Newtonsoft.Json — that is where its API and reputation come from, and the JsonSubTypes NuGet package targets it. A System.Text.Json port exists as the JsonSubTypes.Text.Json package (.NET 8+): it shares the same API but is experimental. Full documentation, differences and known limitations are in the dedicated section at the bottom: System.Text.Json variant.

DeserializeObject with custom type property name

[JsonConverter(typeof(JsonSubtypes), "Kind")]
public interface IAnimal
{
    string Kind { get; }
}

public class Dog : IAnimal
{
    public string Kind { get; } = "Dog";
    public string Breed { get; set; }
}

public class Cat : IAnimal {
    public string Kind { get; } = "Cat";
    public bool Declawed { get; set;}
}

The second parameter of the JsonConverter attribute is the JSON property name that will be use to retreive the type information from JSON.

var animal = JsonConvert.DeserializeObject<IAnimal>("{\"Kind\":\"Dog\",\"Breed\":\"Jack Russell Terrier\"}");
Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed);

N.B.: This only works for types in the same assembly as the base type/interface and either in the same namespace or with a fully qualified type name.

DeserializeObject with custom type mapping

[JsonConverter(typeof(JsonSubtypes), "Sound")]
[JsonSubtypes.KnownSubType(typeof(Dog), "Bark")]
[JsonSubtypes.KnownSubType(typeof(Cat), "Meow")]
public class Animal
{
    public virtual string Sound { get; }
    public string Color { get; set; }
}

public class Dog : Animal
{
    public override string Sound { get; } = "Bark";
    public string Breed { get; set; }
}

public class Cat : Animal
{
    public override string Sound { get; } = "Meow";
    public bool Declawed { get; set; }
}
var animal = JsonConvert.DeserializeObject<IAnimal>("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}");
Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed);

N.B.: Also works with other kind of value than string, i.e.: enums, int, ...

SerializeObject and DeserializeObject with custom type property only present in JSON

This mode of operation only works when JsonSubTypes is explicitely registered in JSON.NET's serializer settings, and not through the [JsonConverter] attribute.

public abstract class Animal
{
    public int Age { get; set; }
}

public class Dog : Animal
{
    public bool CanBark { get; set; } = true;
}

public class Cat : Animal
{
    public int Lives { get; set; } = 7;
}

public enum AnimalType
{
    Dog = 1,
    Cat = 2
}

Registration:

var settings = new JsonSerializerSettings();
settings.Converters.Add(JsonSubtypesConverterBuilder
    .Of(typeof(Animal), "Type") // type property is only defined here
    .RegisterSubtype(typeof(Cat), AnimalType.Cat)
    .RegisterSubtype(typeof(Dog), AnimalType.Dog)
    .SerializeDiscriminatorProperty() // ask to serialize the type property
    .Build());

or using syntax with generics:

var settings = new JsonSerializerSettings();
settings.Converters.Add(JsonSubtypesConverterBuilder
    .Of<Animal>("Type") // type property is only defined here
    .RegisterSubtype<Cat>(AnimalType.Cat)
    .RegisterSubtype<Dog>(AnimalType.Dog)
    .SerializeDiscriminatorProperty() // ask to serialize the type property
    .Build());

De-/Serialization:

var cat = new Cat { Age = 11, Lives = 6 }

var json = JsonConvert.SerializeObject(cat, settings);

Assert.Equal("{\"Lives\":6,\"Age\":11,\"Type\":2}", json);

var result = JsonConvert.DeserializeObject<Animal>(json, settings);

Assert.Equal(typeof(Cat), result.GetType());
Assert.Equal(11, result.Age);
Assert.Equal(6, (result as Cat)?.Lives);

DeserializeObject mapping by property presence

[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Employee), "JobTitle")]
[JsonSubtypes.KnownSubTypeWithProperty(typeof(Artist), "Skill")]
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : Person
{
    public string Department { get; set; }
    public string JobTitle { get; set; }
}

public class Artist : Person
{
    public string Skill { get; set; }
}

or using syntax with generics:

string json = "[{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
                "{\"Department\":\"Department1\",\"JobTitle\":\"JobTitle1\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}," +
                "{\"Skill\":\"Painter\",\"FirstName\":\"FirstName1\",\"LastName\":\"LastName1\"}]";


var persons = JsonConvert.DeserializeObject<IReadOnlyCollection<Person>>(json);
Assert.AreEqual("Painter", (persons.Last() as Artist)?.Skill);

Registration:

settings.Converters.Add(JsonSubtypesWithPropertyConverterBuilder
    .Of(typeof(Person))
    .RegisterSubtypeWithProperty(typeof(Employee), "JobTitle")
    .RegisterSubtypeWithProperty(typeof(Artist), "Skill")
    .Build());

or

settings.Converters.Add(JsonSubtypesWithPropertyConverterBuilder
    .Of<Person>()
    .RegisterSubtypeWithProperty<Employee>("JobTitle")
    .RegisterSubtypeWithProperty<Artist>("Skill")
    .Build());

A default class other than the base type can be defined

[JsonConverter(typeof(JsonSubtypes))]
[JsonSubtypes.KnownSubType(typeof(ConstantExpression), "Constant")]
[JsonSubtypes.FallBackSubType(typeof(UnknownExpression))]
public interface IExpression
{
    string Type { get; }
}

Or with code configuration:

settings.Converters.Add(JsonSubtypesConverterBuilder
    .Of(typeof(IExpression), "Type")
    .SetFallbackSubtype(typeof(UnknownExpression))
    .RegisterSubtype(typeof(ConstantExpression), "Constant")
    .Build());
settings.Converters.Add(JsonSubtypesWithPropertyConverterBuilder
    .Of(typeof(IExpression))
    .SetFallbackSubtype(typeof(UnknownExpression))
    .RegisterSubtype(typeof(ConstantExpression), "Value")
    .Build());

System.Text.Json variant

Status: experimental. The JsonSubTypes.Text.Json package is a release candidate (1.0.0-rc.x) and not yet part of the project's stable offering. The code is fully tested (133 unit tests) and the API is complete, but the stable 1.0.0 release will follow once the package has been exercised in more real-world projects.

A variant of the library for System.Text.Json (.NET 8+) is available in the JsonSubTypes.Text.Json namespace and package. It supports the same attribute-driven and builder-driven API, adapted to System.Text.Json idioms.

Attribute based discriminator

using JsonSubTypes.Text.Json;

[JsonSubTypeConverter(typeof(JsonSubtypes<Animal>), "Sound")]
[KnownSubType(typeof(Dog), "Bark")]
[KnownSubType(typeof(Cat), "Meow")]
public class Animal
{
    public virtual string Sound { get; }
    public string Color { get; set; }
}

public class Dog : Animal
{
    public override string Sound { get; } = "Bark";
    public string Breed { get; set; }
}

public class Cat : Animal
{
    public override string Sound { get; } = "Meow";
    public bool Declawed { get; set; }
}
var animal = JsonSerializer.Deserialize<Animal>("{\"Sound\":\"Bark\",\"Breed\":\"Jack Russell Terrier\"}");
Assert.AreEqual("Jack Russell Terrier", (animal as Dog)?.Breed);

Like the native [JsonDerivedType] polymorphism, the attribute-based converter handles both directions: serializing through the base type writes the discriminator, and deserialization reads it back, so round-trips work out of the box:

var json = JsonSerializer.Serialize<Animal>(new Dog { Breed = "Jack Russell Terrier" });
// {"Sound":"Bark","Breed":"Jack Russell Terrier"}
var back = JsonSerializer.Deserialize<Animal>(json);
Assert.IsInstanceOf<Dog>(back);

When the runtime type is not declared in the [KnownSubType] mappings (e.g. a multi-level hierarchy where the leaf is registered on an intermediate base), serialization falls back to the plain runtime-type contract without a discriminator.

Builder based dynamic registration

var options = new JsonSerializerOptions();
options.Converters.Add(JsonSubtypesConverterBuilder
    .Of(typeof(Animal), "type")
    .RegisterSubtype(typeof(Cat), AnimalType.Cat)
    .RegisterSubtype(typeof(Dog), AnimalType.Dog)
    .Build());

var result = JsonSerializer.Deserialize<Animal>("{\"catLives\":6,\"type\":2,\"age\":11}", options);
Assert.AreEqual(typeof(Cat), result.GetType());

Serializing the discriminator

The attribute-based converter writes the discriminator by default. For the builder, writing the discriminator is opt-in, like the Newtonsoft version:

options.Converters.Add(JsonSubtypesConverterBuilder
    .Of(typeof(Animal), "type")
    .SerializeDiscriminatorProperty()                 // discriminator first (default)
    // or .SerializeDiscriminatorProperty(false)      // discriminator last
    .RegisterSubtype(typeof(Cat), AnimalType.Cat)
    .RegisterSubtype(typeof(Dog), AnimalType.Dog)
    .Build());

var json = JsonSerializer.Serialize<Animal>(new Cat { Age = 11, Lives = 6 }, options);
// {"type":2,"catLives":6,"age":11}

As with the native [JsonDerivedType] polymorphism, serialization must go through the base type (or a base-typed property/collection) for the converter and the discriminator to apply. Serializing a value with a concrete subtype as its static type bypasses the converter, and serializing an unregistered type throws when SerializeDiscriminatorProperty() is used.

Mapping by property presence

[JsonSubTypeConverter(typeof(JsonSubtypes<Person>))]
[KnownSubTypeWithProperty(typeof(Employee), "JobTitle")]
[KnownSubTypeWithProperty(typeof(Artist), "Skill")]
public class Person { }

Fallback subtype

[JsonSubTypeConverter(typeof(JsonSubtypes<IExpression>), "Type")]
[KnownSubType(typeof(ConstantExpression), "Constant")]
[FallBackSubType(typeof(UnknownExpression))]
public interface IExpression { }

Differences with the Newtonsoft.Json version

  • The attribute-based converter writes the discriminator by default (like the native [JsonDerivedType] polymorphism), whereas the Newtonsoft version never writes it from attributes (CanWrite = false). With the builder, writing is opt-in via SerializeDiscriminatorProperty().
  • With System.Text.Json, the converter is only applied when the static type is the polymorphic base type (or a base-typed property/collection), matching the native [JsonDerivedType] behavior. The Newtonsoft version also applies converters when serializing a value whose static type is a concrete subtype.
  • A property declared with a base class or interface type is serialized using the declared type's contract: subtype members are omitted unless a converter that claims the declared type is applied (attribute on the type, or builder registered in JsonSerializerOptions). The Newtonsoft version serialized the runtime type by default.
  • Property order differs: System.Text.Json emits properties most-derived-first, while the Newtonsoft version honored [JsonProperty(Order = N)]. There is no Order support in System.Text.Json.
  • Deeply nested graphs need MaxDepth about one level higher than with the Newtonsoft/plain serialization: the discriminator write path round-trips through a JsonDocument, which consumes one depth level. (A 64-level chain requires MaxDepth = 66 instead of 65.)
  • Name-based type resolution stays scoped to the base type's assembly by default. Cross-assembly subtypes require an explicit opt-in: JsonSubTypesTypeResolution.AddAssembly(...), a capability the Newtonsoft version does not have.
  • JsonNamingPolicy and PropertyNameCaseInsensitive are respected when matching the discriminator property, and JsonStringEnumConverter is respected when mapping discriminator values. Note that JsonStringEnumConverter (.NET 8) does not honor [EnumMember(Value = ...)] — use enum names or [JsonStringEnumMemberName] (.NET 9+).
  • Dotted or nested discriminator property paths (e.g. "nested.property") are supported.
  • Fallback paths: serializing the base type itself (rather than a subtype) and deserializing an unknown discriminator back to the base use a reflection-based writer/reader, because the base type's contract is owned by the converter (System.Text.Json exposes no property metadata for converter-owned types). [JsonPropertyName], [JsonIgnore] (including JsonIgnoreCondition), the naming policy and DefaultIgnoreCondition are honored; per-property [JsonConverter], [JsonInclude] fields, required members and parameterized constructors are not supported on these two paths.
  • Performance: writing an object with a discriminator serializes it once, then re-parses the JSON (JsonDocument) to inject the discriminator property, so payloads spend roughly 2-3x their size in temporary memory on the write path. This is the cost of the converter architecture and of the MaxDepth + 1 note above.
  • Security: name-based subtype resolution (GetTypeByName, used when no [KnownSubType] mapping is declared) resolves a type name from the JSON discriminator against the base type's assembly (and any assembly registered via JsonSubTypesTypeResolution). Only types assignable from the base can be resolved, but do not expose a name-based hierarchy to untrusted JSON without validating the payload upstream.
  • The property-presence builder (JsonSubtypesWithPropertyConverterBuilder) registers subtypes by property name, so two subtypes cannot share the same property name through the builder (use [KnownSubTypeWithProperty] attributes for that case).

Native [JsonDerivedType] vs JsonSubTypes.Text.Json

Feature / Capability Native STJ ([JsonDerivedType]) JsonSubTypes.Text.Json
Type discriminator mapping
Custom discriminator property name
Property presence matching (KnownSubTypeWithProperty)
Fallback subtype (FallBackSubType)
Cross-assembly / Plugin type resolution
Dotted / nested discriminator path ("nested.type")
Opt-in discriminator writing (SerializeDiscriminatorProperty)
Seamless migration from Newtonsoft.Json JsonSubTypes
Native AOT / Trimming support ⚠️ (Requires reflection)

Known Scope & Fallback Path Behavior

To preserve full compatibility with advanced features (KnownSubTypeWithProperty, nested discriminator paths, enum/null discriminators, cross-assembly resolution) while delegating 99% of object serialization to System.Text.Json, the library isolates base-type serialization to two narrow paths (when serializing the base type directly or reading an unregistered fallback type):

  1. Subtypes (99% of cases): Full delegation to System.Text.Json. All STJ attributes ([JsonIgnore], [JsonInclude], property [JsonConverter], [JsonConstructor], record types, naming policies) are fully supported natively.
  2. Base-as-leaf & Fallback path: Handled via lightweight direct property mapping. Standard attributes ([JsonIgnore], [JsonPropertyName], PropertyNamingPolicy, PropertyNameCaseInsensitive) are honored. Advanced member-level STJ attributes (e.g. [JsonInclude] on fields, [JsonConverter] on individual base properties, parameterized constructors) on the fallback base type itself are intentionally not re-implemented to avoid duplicate serializer engine complexity.
  • Parameterless Constructor for Fallback: The base fallback type requires a parameterless constructor. Subtypes resolved via discriminator mapping support all STJ constructor features (primary constructors, record types).
  • Native AOT: Relies on reflection to discover subtypes; annotated with [RequiresUnreferencedCode] and [RequiresDynamicCode].

Native [JsonDerivedType] or JsonSubTypes.Text.Json?

Use case Recommended
Closed hierarchy, all subtypes known at compile time, string/int discriminator, round-trip serialization, Native AOT Native [JsonDerivedType] / [JsonPolymorphic] (source-gen friendly)
Discriminator by property presence (no discriminator field in the JSON) JsonSubTypes.Text.Json
Open hierarchies / subtypes registered at runtime JsonSubTypes.Text.Json
Non string/int discriminator values (enums, null, several values mapping to one type) JsonSubTypes.Text.Json
Nested or dotted discriminator paths (e.g. "nested.property") JsonSubTypes.Text.Json
Resolution by .NET type name, or cross-assembly plugin subtypes JsonSubTypes.Text.Json
Migrating an existing JsonSubTypes/Newtonsoft code base JsonSubTypes.Text.Json (same API)

💖 Support this project

If this project helped you save money or time or simply makes your life also easier, you can give me a cup of coffee =)

  • Support via PayPal
  • Bitcoin — You can send me bitcoins at this address: 33gxVjey6g4Beha26fSQZLFfWWndT1oY3F

License

FOSSA Status

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp1.0 was computed.  netcoreapp1.1 was computed.  netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard1.3 is compatible.  netstandard1.4 was computed.  netstandard1.5 was computed.  netstandard1.6 was computed.  netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net35 is compatible.  net40 is compatible.  net403 was computed.  net45 is compatible.  net451 was computed.  net452 was computed.  net46 is compatible.  net461 was computed.  net462 was computed.  net463 was computed.  net47 is compatible.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen30 was computed.  tizen40 was computed.  tizen60 was computed. 
Universal Windows Platform uap was computed.  uap10.0 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (865)

Showing the top 5 NuGet packages that depend on JsonSubTypes:

Package Downloads
Okta.Sdk

Official .NET SDK for the Okta API

Xero.NetStandard.OAuth2

This is a .NETStandard SDK library, used to communicate with the Xero API using OAuth2.0. See https://github.com/XeroAPI/Xero-NetStandard for more information

InfluxDB.Client

The reference client that allows query, write and management (bucket, organization, users) for the InfluxDB 2.x.

sib_api_v3_sdk

Official SendinBlue provided RESTFul API V3 C# Library

Finbourne.EdpLusidSdk

LUSID SDK for EDP

GitHub repositories (25)

Showing the top 20 popular GitHub repositories that depend on JsonSubTypes:

Repository Stars
BililiveRecorder/BililiveRecorder
录播姬 | mikufans 生放送录制
Azure-Samples/cognitive-services-speech-sdk
Sample code for the Microsoft Cognitive Services Speech SDK
antonpup/Aurora
Unified lighting effects across multiple brands and various games.
jlucansky/Quartzmin
Quartzmin is powerful, easy to use web management tool for Quartz.NET
api-bricks/api-bricks-sdk
SDKs for CoinAPI & FinFeedAPI
takuya-takeuchi/DlibDotNet
Dlib .NET wrapper written in C++ and C# for Windows, MacOS, Linux and iOS
blish-hud/Blish-HUD
A Guild Wars 2 overlay with extreme extensibility through compiled modules.
IoTSharp/SilkierQuartz
SilkierQuartz can host jobs using HostService and Provide a web management tools for Quartz !
influxdata/influxdb-client-csharp
InfluxDB 2.x C# Client
microsoft/verisol
A formal verifier and analysis tool for Solidity Smart Contracts
alipay/alipay-sdk-net-all
支付宝开放平台 Alipay SDK for .NET
notion-dotnet/notion-sdk-net
A Notion SDK for .Net
Anapher/Strive
Open source video conferencing platform
blockchain/lib-exchange-client
christianhelle/apiclientcodegen
A collection of Visual Studio code generators for Swagger / OpenAPI specification files
LexPredict/lexpredict-contraxsuite
LexPredict ContraxSuite
okta/okta-sdk-dotnet
A .NET SDK for interacting with the Okta management API, enabling server-side code to manage Okta users, groups, applications, and more.
kameleo-io/kameleo
Anti-detect browser for web scraping and automation. Engine-level fingerprint masking for Chromium and Firefox. Self-hosted, Docker-ready. Integrates with Selenium, Playwright, and Puppeteer via SDKs in Python, JavaScript, and C#.
bybit-exchange/api-connectors
Libraries for connecting to the Bybit API.
James231/Start-Menu-Manager
App to add websites/software/files/folders/scripts to the Windows 10 Start Menu and Taskbar, and priority shortcuts to Windows 10 Search.
Version Downloads Last Updated
2.1.0 25 8/11/2026
2.0.1 59,611,095 10/18/2022
2.0.0 45,803 10/18/2022
1.9.0 11,626,021 5/9/2022
1.8.0 32,262,640 9/23/2020
1.7.0 4,390,124 3/28/2020
1.6.0 5,652,924 6/24/2019
1.5.2 10,158,264 1/19/2019
1.5.1 781,331 10/15/2018
1.5.0 107,787 8/27/2018
1.4.0 292,066 4/17/2018
1.3.1 11,861 4/12/2018
1.3.0 92,534 1/28/2018
1.2.0 14,012,056 11/22/2017
1.1.3 75,449 11/15/2017
1.1.2 8,467 10/20/2017
1.1.1 7,864 9/21/2017
1.1.0 12,877 9/19/2017
1.0.0 32,201 7/23/2017