Home > database >  How do I avoid writing `$type` with System.Text.Json?
How do I avoid writing `$type` with System.Text.Json?

Time:09-12

How do I avoid exposing a type discriminator when serializing a class that has opted into polymorphic deserialization?

I'm using System.Text.Json's JsonDerivedType attributes to do polymorphic deserialization. So I need to specify typeDiscriminator on the attribute, or else deserialization won't read incoming $type fields on the JSON. But when I serialize those same classes, I don't want System.Text.Json to automatically add $type (exposing implmentation details, etc.).

Example

[JsonDerivedType(typeof(Derived1), typeDiscriminator: "Derived1")]
[JsonDerivedType(typeof(Derived2), typeDiscriminator: "Derived2")]
public record BaseType(int Id);

public record Derived1(int Id, string Name) : BaseType(Id);
public record Derived2(int Id, bool IsActive) : BaseType(Id);

When serializing:

var values = new List<BaseType>
{
  new Derived1(123, "Foo"),
  new Derived2(456, true)
};
JsonSerializer.Serialize(values);

Actual output:

[
  { "$type": "Derived1", "Id": 123, "Name": "Foo" },
  { "$type": "Derived2", "Id": 456, "IsActive": true }
]

How do I avoid $type being written?

Desired output:

[
  { "Id": 123, "Name": "Foo" },
  { "Id": 456, "IsActive": true }
]

Again, I know I could exclude typeDiscriminator from the attribute, but then deserialization would not work.

Links

CodePudding user response:

One solution is to write separate overrides of DefaultJsonTypeInfoResolver for serialization and deserialization, and remove the [JsonDerivedType] attributes.

Similar to Newtonsoft, System.Text.Json also supports polymorphic deserialization via the serializer's options as an alternative to attributes. In Newtonsoft, you would use different values of JsonSerializerSettings.TypeNameHandling to affect whether $type is written. System.Text.Json requires that you opt each type into this behavior explicitly, but the idea is the same.

Sample classes:

// Don't add [JsonDerivedType]
public record BaseType(int Id);

public record Derived1(int Id, string Name) : BaseType(Id);
public record Derived2(int Id, bool IsActive) : BaseType(Id);

Resolver for deserializing with a discriminator:

// Example taken from dotnet/runtime#63747 "Configuring Polymorphism via the Contract model"
public class DeserializeResolver : DefaultJsonTypeInfoResolver
{
    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
        if (jsonTypeInfo.Type == typeof(BaseType))
        {
            jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions()
            {
                DerivedTypes =
                {
                    // Pass in the type discriminator for this resolver.
                    new JsonDerivedType(typeof(Derived1), typeDiscriminator: "Derived1"),
                    new JsonDerivedType(typeof(Derived2), typeDiscriminator: "Derived2"),
                }
            };
        }

        return jsonTypeInfo;
    }
}

Resolver for serializing without a discriminator:

public class SerializeResolver : DefaultJsonTypeInfoResolver
{
    ...
    // Exact same as above resolver except:
                DerivedTypes =
                {
                    // Don't pass in the typeDiscriminator parameter.
                    new JsonDerivedType(typeof(Derived1)),
                    new JsonDerivedType(typeof(Derived2)),
                }
    ...
}

Then, use different instances of JsonSerializerOptions serialization and deserialization:

initial:

[
  { "$type": "Derived1", "Name": "Foo", "Id": 123 },
  { "$type": "Derived2", "IsActive": true, "Id": 456 }
]
string initial;

var deserializeOptions = new JsonSerializerOptions
{
    TypeInfoResolver = new DeserializeResolver(),
};
var serializeOptions = new JsonSerializerOptions
{
    TypeInfoResolver = new SerializeResolver(),
};

var values = JsonSerializer.Deserialize<List<BaseType>>(initial, deserializeOptions);

// At this point, the types have been successfully deserialized via their $type.
Assert.NotNull(values);
Assert.IsAssignableFrom<Derived1>(values[0]);
Assert.IsAssignableFrom<Derived2>(values[1]);

string result = JsonSerializer.Serialize(values, serializeOptions);

result:

[
  { "Name": "Foo", "Id": 123 },
  { "IsActive": true, "Id": 456 }
]

CodePudding user response:

I see the following options:

  1. Don't use polymorphic list, e.g. have a list of Derived1 and another list of Derived2
  2. Change $type to something that you like [JsonPolymorphic(CustomTypeDiscriminatorPropertyName = "Kind")]
  3. Use a different name: [JsonDerivedType(typeof(Derived1), typeDiscriminator: "A")], or a number [JsonDerivedType(typeof(Derived1), typeDiscriminator: 1)],
  4. Don't use JsonDerivedType and write your own deserialiser

Source: #63747: Developers can use System.Text.Json to serialize type hierarchies securely

  • Related