Newtonsoft.json: Be able to support custom inheritance discriminator field

Created on 31 May 2017  路  12Comments  路  Source: JamesNK/Newtonsoft.Json

Source JSON

{
  "pets": [
    {
      "className": "Cat",
      "color": "white",
      "breed": "Aidi"
    },
    {
      "className": "Dog",
      "color": "white",
      "declawed": false
    }
  ]
}

destination types

class Root {
    List<Annimal> pets { get; set;}   
}

class Annimal {
    string ClassName { get; set;} // not mandatory
    string Color { get; set;}
}

class Dog : Annimal {
    string breed { get; set;}
}

class Cat : Annimal {
    bool declawed { get; set;}
}

Expected behavior

Be able to serialize the given json with few elegant configuration properties. Concretely if we were able to configure by some way the JsonTypeReflector.TypePropertyName per inheritance hierarchy it might help us.

This would be very helpful to support the

Actual behavior

Actually we are forced to implement some sub-optimal/complex solutions, see: https://stackoverflow.com/questions/9490345/json-net-change-type-field-to-another-name

Most helpful comment

@JamesNK I seems to understand the tradeoff problematic you're dealing with, note that this customization line has been crossed in other popular language libraries such as the Jackson (Java) (example here : https://gist.github.com/christophercurrie/8939489 and is by example leveraged in the Swagger-Codegen). That does not off course means you have to embrace it too but I think the request is not either unreasonable.

All 12 comments

Heck, out there are people using all sort of field names. I'm really curious why is this a constant with no options to override.

Because you have to draw a line at what point you stop allow customization. I have decided to draw the line here.

@JamesNK Ok, but know that this isn't a purely visual preference. There are a lot of different field names used for that purpose and it really hampers json.net in such case when field isn't named $type.
I don't understand why do you insist on a const. Make it a property with a default value, what's the big deal?

@JamesNK I seems to understand the tradeoff problematic you're dealing with, note that this customization line has been crossed in other popular language libraries such as the Jackson (Java) (example here : https://gist.github.com/christophercurrie/8939489 and is by example leveraged in the Swagger-Codegen). That does not off course means you have to embrace it too but I think the request is not either unreasonable.

A workaround implementation can be found here: https://github.com/manuc66/JsonSubTypes

@manuc66 Nice.

I ran into the same problem. It is very problematic to use JSON.NET for deserialization GraphQL responses with interfaces fields because GraphQL uses __typename field discriminator.

In my opinion the choice of a constant for a discriminator is a short-sighted decision. These steps simply prohibit / complicate any adaptation of the library to different environments. Instead of a simple, effective setup, the library itself has to resort to writing unnecessary converters, which complicate the process and reduce productivity.

I solved this problem with a custom JsonConverter. My solution borrows from CustomCreationConverter but instead of making you extend the converter, my converter is sealed and you customize its behaviors via parameters.

Source code for DiscriminatedJsonConverter can be found in my Gist:
https://gist.github.com/StevenLiekens/82ddcf1823ee91cf6d5edfcdb1f6a591

Example usage:

[JsonConverter(typeof(DiscriminatedJsonConverter), typeof(AnimalDiscriminatorOptions))]
public class Animal
{
    private string ClassName { get; set; } // not mandatory
    private string Color { get; set; }
}

// MUST have a parameterless ctor so it can be instantiated with Activator.CreateInstance
public class AnimalDiscriminatorOptions : DiscriminatorOptions
{
    public override Type BaseType => typeof(Animal);

    public override string DiscriminatorFieldName => "className";

    // true if you want to set the value of ClassName, false to skip it
    public override bool SerializeDiscriminator => false;

    public override IEnumerable<(string TypeName, Type Type)> GetDiscriminatedTypes()
    {
        yield return ("Cat", typeof(Cat));
        yield return ("Dog", typeof(Dog));
    }
}

Another issue with the choice of the $type value is that it conflicts when storing a model to MongoDB, since its a MongoDB reserved 'name.

Although it's possible to write a custom converter as seen in this thread, it sure would be nice to be able to change the name value without having to write a custom converter.

I do understand @JamesNK 's point that you need to draw a line somewhere, but perhaps that line can be reconsidered? :)

Not allowing the type discriminator also prevents from properly serializing/deserializing OData formatted documents, which use $odata.type.

@JamesNK - The number of times this flexibility is requested makes your choice to _"draw the line"_ a little odd.

Today, working with another ecosystem and I can't just force _your_ convention upon it. Yet somehow, I need to be compatible. Wat du? :laughing:

I understand in software when people need to draw a line, but this is usually done in response to adding novel functionality. Not in imposing limitations, which you must admit makes _drawing the line_ a bit disingenuous.

Please reconsider.

@atrauzzi I used Linq-to-Json to overcome that limitation in my mobile client app.

using ( var bsonReader = new BsonDataReader( await response.Content.ReadAsStreamAsync(), false, DateTimeKind.Utc ) ) {
    var doc = new JsonSerializer().Deserialize<JObject>( bsonReader );
    if ( doc == null ) return default;
    foreach ( var obj in doc.DescendantsAndSelf().OfType<JObject>() ) {
        var property = obj.Properties().SingleOrDefault( p => p.Name == "_id" );
        string id = Hex.ToString( (byte[])property?.Value );
        property?.Replace( new JProperty( "$id", id ) );

        property = obj.Properties().SingleOrDefault( p => p.Name == "_t" );
        if ( id != null )
            property.AddAfterSelf( new JProperty( "id", id ) );
        property?.Replace( new JProperty( "$type", (string)property.Value ) );
    }
    foreach ( var reference in doc.Descendants().OfType<JValue>().Where( v => v.Type == JTokenType.Bytes ).ToList() )
        reference.Replace( new JObject( new JProperty( "$ref", Hex.ToString( (byte[])reference ) ) ) );
    RepositionDefinitions( doc );
    object result = doc.ToObject<object>( JsonSerializer.Create( jSettings ) );
    cancelToken.ThrowIfCancellationRequested();
    return result;
}

Now I'm adding a web client and will remove the above code. I was trying to be efficient by serving BSON straight from MongoDB but, since the browser is slow with BSON, I'll instead serve it JSON. Because of this, I may as well deserialize the BSON into POCOs before reserializing it for transport to the client. What will I use to reserialize? JSON.NET of course. So I'll no longer need that client-side preprocessing.

Basically, I'm giving up on my custom toolchain for transporting data from server to client. I generally like to innovate but I've concluded that this isn't an area in which to do it. It's not worth the aggravation since it'll make communicating with new systems more difficult, and it'll be more work than you expected.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dmoeff picture dmoeff  路  17Comments

Richard-Payne picture Richard-Payne  路  13Comments

TylerBrinkley picture TylerBrinkley  路  37Comments

bytenik picture bytenik  路  11Comments

Phreak87 picture Phreak87  路  11Comments