Rom Eh
Rom Eh

Reputation: 2063

Serialize objects implementing interface with System.Text.Json

I have a master class which contains a generic collection. Elements in the collection are of diffetent types, and each implements an interface.

Master class:

public class MasterClass
{
    public ICollection<IElement> ElementCollection { get; set; }
}

Contract for the elements:

public interface IElement
{
    string Key { get; set; }
}

Two samples for the elements:

public class ElementA : IElement
{
    public string Key { get; set; }

    public string AValue { get; set; }
}

public class ElementB : IElement
{
    public string Key { get; set; }

    public string BValue { get; set; }
}

I need to serialize an instance of MasterClass object using the new System.Text.Json library in Json. Using the following code,

public string Serialize(MasterClass masterClass)
{
    var options = new JsonSerializerOptions
    {
        WriteIndented = true,
    };
    return JsonSerializer.Serialize(masterClass, options);
}

I get the follwing JSON:

{
    "ElementCollection":
    [
        {
            "Key": "myElementAKey1"
        },
        {
            "Key": "myElementAKey2"
        },
        {
            "Key": "myElementBKey1"
        }
    ]
}

instead of:

{
    "ElementCollection":
    [
        {
            "Key": "myElementAKey1",
            "AValue": "MyValueA-1"
        },
        {
            "Key": "myElementAKey2",
            "AValue": "MyValueA-2"
        },
        {
            "Key": "myElementBKey1",
            "AValue": "MyValueB-1"
        }
    ]
}

Which class (converter, writer, ...)should I implement to obtain the complete JSON ?

Thanks in advance for your help.

Upvotes: 17

Views: 19808

Answers (8)

Hakan Deryal
Hakan Deryal

Reputation: 2903

If you want a more generic solution that allows you to specify that you want to serialize a type as its concrete type (which is the default in Json.NET) you can use the following marker interface and converter.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)]
public class JsonSerializeAsConcreteType : Attribute;

public class SerializeAsConcreteTypeConverter : JsonConverter<object>
{
    public override bool CanConvert(Type typeToConvert) => Attribute.IsDefined(typeToConvert, typeof(JsonSerializeAsConcreteType), false);

    public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value as object, options);
    }
}

Note: This only works for serialization, for polymorphic deserialization you should use polymorphic type hierarchy serialization and deserialization .

Upvotes: 0

NTE
NTE

Reputation: 53

Working further on the idea of Rom Eh. In my case, I don't always know the implementation objects. But it turns out you can just cast to an object and all of its properties will be serialized.

(System.Text.Json.Serialization.JsonConverter) :

public class ElementConverter : JsonConverter<IElement>
{
    public override IElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, IElement value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, value as object, options);
    }
}

This just needs some more work for the Read method.

Upvotes: 3

t00thy
t00thy

Reputation: 519

I have currently been infront of the same problem in Blazor app, so I was not been able to switch to Newtonsoft.Json easily. I found two ways. One is in reality hack. You can create custom converter, where you use Newtonsoft.Json in Read/Write methods, instead of System.Text.Json. But that was not what I was looking for. So I make some custom interface converter. I have some working solution, that not have been tested widely, but it's working for what I need.

Situation

I have a List<TInterface> with objects implementing TInterface. But there is a lot of different implementations. I need to serialize data on server, and deserialize on client WASM app, with all the data. For JavaScript deserialization, the implementation with custom Write method mentioned later is enough. For deserialization in C#, I need to know the exact types of objects serialized for each item in the list.

First, I need JsonConverterAttribute on interface. So I was following this article: https://khalidabuhakmeh.com/serialize-interface-instances-system-text-json. There is some implementation of Writer, that will handle interface type. But there is not Read implementation. So I had to make my own.

How

  • modify Write method to write type of object as first property to JSON object. Using JsonDocument to get all properties from original object.
  • when reading the JSON, use clonned reader (as suggested in Microsoft docs for custom json converters) to find first property named $type with type information. Than create instance of that type and use type to deserialize data from original reader.

Code

Interface and classes:

[JsonInterfaceConverter(typeof(InterfaceConverter<ITest>))]
public interface ITest
{
    int Id { get; set; }
    string Name { get; set; }
}

public class ImageTest : ITest
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Image { get; set; } = string.Empty;
}

public class TextTest : ITest
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Text { get; set; } = string.Empty;
    public bool IsEnabled { get; set; }
}

Interface converter attribute:

// Source: https://khalidabuhakmeh.com/serialize-interface-instances-system-text-json
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
    public JsonInterfaceConverterAttribute(Type converterType)
        : base(converterType)
    {
    }
}

Converter:

public class InterfaceConverter<T> : JsonConverter<T>
    where T : class
{
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Utf8JsonReader readerClone = reader;
        if (readerClone.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.PropertyName)
        {
            throw new JsonException();
        }

        string propertyName = readerClone.GetString();
        if (propertyName != "$type")
        {
            throw new JsonException();
        }

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.String)
        {
            throw new JsonException();
        }

        string typeValue = readerClone.GetString();
        var instance = Activator.CreateInstance(Assembly.GetExecutingAssembly().FullName, typeValue).Unwrap();
        var entityType = instance.GetType();

        var deserialized = JsonSerializer.Deserialize(ref reader, entityType, options);
        return (T)deserialized;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        switch (value)
        {
            case null:
                JsonSerializer.Serialize(writer, (T)null, options);
                break;
            default:
                {
                    var type = value.GetType();
                    using var jsonDocument = JsonDocument.Parse(JsonSerializer.Serialize(value, type, options));
                    writer.WriteStartObject();
                    writer.WriteString("$type", type.FullName);

                    foreach (var element in jsonDocument.RootElement.EnumerateObject())
                    {
                        element.WriteTo(writer);
                    }

                    writer.WriteEndObject();
                    break;
                }
        }
    }
}

Usage:

    var list = new List<ITest>
    {
        new ImageTest { Id = 1, Name = "Image test", Image = "some.url.here" },
        new TextTest { Id = 2, Name = "Text test", Text = "kasdglaskhdgl aksjdgl asd gasdg", IsEnabled = true },
        new TextTest { Id = 3, Name = "Text test 2", Text = "asd gasdg", IsEnabled = false },
        new ImageTest { Id = 4, Name = "Second image", Image = "diff.url.here" }
    };

    var json = JsonSerializer.Serialize(list);
    var data = JsonSerializer.Deserialize<List<ITest>>(json);

    // JSON data
    // [
    //   {
    //      "$type":"ConsoleApp1.ImageTest",
    //      "Id":1,
    //      "Name":"Image test",
    //      "Image":"some.url.here"
    //   },
    //   {
    //      "$type":"ConsoleApp1.TextTest",
    //      "Id":2,
    //      "Name":"Text test",
    //      "Text":"kasdglaskhdgl aksjdgl asd gasdg",
    //      "IsEnabled":true
    //   },
    //   {
    //      "$type":"ConsoleApp1.TextTest",
    //      "Id":3,
    //      "Name":"Text test 2",
    //      "Text":"asd gasdg",
    //      "IsEnabled":false
    //   },
    //   {
    //      "$type":"ConsoleApp1.ImageTest",
    //      "Id":4,
    //      "Name":"Second image",
    //      "Image":"diff.url.here"
    //   }
    // ]

Edit: I made a NuGet package with this logic. You can download it here: InterfaceConverter.SystemTextJson

Edit 26.3.2022: The NuGet package version has implemented more logic, eg. looking for type in all referenced assemblies.

Upvotes: 4

Muhammad Waqas Aziz
Muhammad Waqas Aziz

Reputation: 851

Improved On @t00thy Solution

Your solution is nice but what if concrete type in an other assembly?

Converter Class

public class InterfaceConverter<T> : JsonConverter<T> where T : class
{
    public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        Utf8JsonReader readerClone = reader;
        if (readerClone.TokenType != JsonTokenType.StartObject)
            throw new JsonException("Problem in Start object! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.PropertyName)
            throw new JsonException("Token Type not equal to property name! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        string? propertyName = readerClone.GetString();
        if (string.IsNullOrWhiteSpace(propertyName) || propertyName != "$type")
            throw new JsonException("Unable to get $type! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        readerClone.Read();
        if (readerClone.TokenType != JsonTokenType.String)
            throw new JsonException("Token Type is not JsonTokenString! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        string? typeValue = readerClone.GetString();
        if(string.IsNullOrWhiteSpace(typeValue))
            throw new JsonException("typeValue is null or empty string! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        string? asmbFullName = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(ass => !string.IsNullOrEmpty(ass.GetName().Name) && ass.GetName().Name.Equals(typeValue.Split(" ")[1]))?.FullName;

        if (string.IsNullOrWhiteSpace(asmbFullName))
            throw new JsonException("Assembly name is null or empty string! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        ObjectHandle? instance = Activator.CreateInstance(asmbFullName, typeValue.Split(" ")[0]);
        if(instance == null)
            throw new JsonException("Unable to create object handler! Handler is null! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        object? unwrapedInstance = instance.Unwrap();
        if(unwrapedInstance == null)
            throw new JsonException("Unable to unwrap instance! Or instance is null! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        Type? entityType = unwrapedInstance.GetType();
        if(entityType == null)
            throw new JsonException("Instance type is null! Or instance is null! method: " + nameof(Read) + " class :" + nameof(InterfaceConverter<T>));

        object? deserialized = JsonSerializer.Deserialize(ref reader, entityType, options);
        if(deserialized == null)
            throw new JsonException("De-Serialized object is null here!");

        return (T)deserialized;
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        switch (value)
        {
            case null:
                JsonSerializer.Serialize(writer, typeof(T) ,options);
                break;
            default:
                {
                    var type = value.GetType();
                    using var jsonDocument = JsonDocument.Parse(JsonSerializer.Serialize(value, type, options));
                    writer.WriteStartObject();
                    writer.WriteString("$type", type.FullName + " " + type.Assembly.GetName().Name);

                    foreach (var element in jsonDocument.RootElement.EnumerateObject())
                    {
                        element.WriteTo(writer);
                    }

                    writer.WriteEndObject();
                    break;
                }
        }
    }
}

Converter Attribute

[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
    public JsonInterfaceConverterAttribute(Type converterType)
        : base(converterType)
    {
    }
}

Interfaces and Classes

[JsonInterfaceConverter(typeof(InterfaceConverter<IUser>))]
public interface IUser
{
    int Id { get; set; }
    string Name { get; set; }
    IEnumerable<IRight> Rights { get; set; }
}

[JsonInterfaceConverter(typeof(InterfaceConverter<IRight>))]
public interface IRight
{
    int Id { get; set; }
    bool HasRight { get; set; }
}

public class User : IUser
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public IEnumerable<IRight> Rights { get; set; } = Enumerable.Empty<IRight>();
}

public class Right : IRight
{
    public int Id { get; set; }
    public bool HasRight { get; set; }
}

Usage:

        //           your dependency injector
        IUser user = IServiceProvider.GetRequiredService<IUser>();
        user.Id = 1;
        user.Name = "Xyz";

        List<IRight> rights = new ();
        //           your dependency injector
        IRight right1 = IServiceProvider.GetRequiredService<IRight>();
        right1.Id = 1;
        right1.HasRight = true;
        rights.Add(right1);
        //           your dependency injector
        IRight right2 = IServiceProvider.GetRequiredService<IRight>();
        right2.Id = 2;
        right2.HasRight = true;
        rights.Add(right2);
        //           your dependency injector
        IRight right3 = IServiceProvider.GetRequiredService<IRight>();
        right3.Id = 1;
        right3.HasRight = true;
        rights.Add(right2);

        var serializedRights = JsonSerializer.Serialize(rights);

        user.Rights = rights;

        // Serialization is simple
        var serilizedUser = JsonSerializer.Serialize(user);

        //But for DeSerialization of single object you need to use it some thing like this
        //                                                    Ask your dependency injector to resolve and get type of object
        IUser usr = JsonSerializer.Deserialize(serilizedUser, IServiceProvider.GetRequiredService<IUser>().GetType());

        //DeSerialization of list or enumerable is simple
        IEnumerable<IRight>? rits = JsonSerializer.Deserialize<IEnumerable<IRight>>(serializedRights);

Upvotes: 0

Rom Eh
Rom Eh

Reputation: 2063

The solution is to implement a generic converter (System.Text.Json.Serialization.JsonConverter) :

public class ElementConverter : JsonConverter<IElement>
{
    public override IElement Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, IElement value, JsonSerializerOptions options)
    {
        if (value is ElementA)
            JsonSerializer.Serialize(writer, value as ElementA, typeof(ElementA), options);
        else if (value is ElementB)
            JsonSerializer.Serialize(writer, value as ElementB, typeof(ElementB), options);
        else
            throw new ArgumentOutOfRangeException(nameof(value), $"Unknown implementation of the interface {nameof(IElement)} for the parameter {nameof(value)}. Unknown implementation: {value?.GetType().Name}");
    }
}

This just needs some more work for the Read method.

Upvotes: 4

Tom Khoury
Tom Khoury

Reputation: 11

I have had the same issue, but my problem may not have been related to yours. It turns out that each object to which the incomming JSON data must be serialized requires a constructor with no arguments. All my objects had constructors with all arguments (to make it easier to create and populate them from a database).

Upvotes: 1

Liam Kernighan
Liam Kernighan

Reputation: 2524

What you're looking for is called polymorphic serialization.

Here's Microsoft documentation article

Here's another question about it

According to documentation you just need to cast your interface to an object. For example:

public class TreeRow
{
    [JsonIgnore]
    public ICell[] Groups { get; set; } = new ICell[0];

    [JsonIgnore]
    public ICell[] Aggregates { get; set; } = new ICell[0];

    [JsonPropertyName("Groups")]
    public object[] JsonGroups => Groups;

    [JsonPropertyName("Aggregates")]
    public object[] JsonAggregates => Aggregates;


    public TreeRow[] Children { get; set; } = new TreeRow[0];
}

Upvotes: 9

Shimmy Weitzhandler
Shimmy Weitzhandler

Reputation: 104771

This works for me:

public class TypeMappingConverter<TType, TImplementation> : JsonConverter<TType>
  where TImplementation : TType
{
  [return: MaybeNull]
  public override TType Read(
    ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
      JsonSerializer.Deserialize<TImplementation>(ref reader, options);

  public override void Write(
    Utf8JsonWriter writer, TType value, JsonSerializerOptions options) =>
      JsonSerializer.Serialize(writer, (TImplementation)value!, options);
}

Usage:

var options =
   new JsonSerializerOptions 
   {
     Converters = 
     {
       new TypeMappingConverter<BaseType, ImplementationType>() 
     }
   };

JsonSerializer.Deserialize<Wrapper>(value, options);

Tests:

[Fact]
public void Should_serialize_references()
{
  // arrange
  var inputEntity = new Entity
  {
    References =
    {
      new Reference
      {
        MyProperty = "abcd"
      },
      new Reference
      {
        MyProperty = "abcd"
      }
    }
  };

  var options = new JsonSerializerOptions
  {
    WriteIndented = true,
    Converters =
    {
      new TypeMappingConverter<IReference, Reference>()
    }
  };

      var expectedOutput =
@"{
  ""References"": [
    {
      ""MyProperty"": ""abcd""
    },
    {
      ""MyProperty"": ""abcd""
    }
  ]
}";

  // act
  var actualOutput = JsonSerializer.Serialize(inputEntity, options);

  // assert
  Assert.Equal(expectedOutput, actualOutput);
}

[Fact]
public void Should_deserialize_references()
{
  // arrange

  var inputJson =
@"{
  ""References"": [
    {
      ""MyProperty"": ""abcd""
    },
    {
      ""MyProperty"": ""abcd""
    }
  ]
}";

  var expectedOutput = new Entity
  {
    References =
    {
      new Reference
      {
        MyProperty = "abcd"
      },
      new Reference
      {
        MyProperty = "abcd"
      }
    }
  };

  var options = new JsonSerializerOptions
  {
    WriteIndented = true
  };

  options.Converters.AddTypeMapping<IReference, Reference>();

  // act
  var actualOutput = JsonSerializer.Deserialize<Entity>(inputJson, options);

  // assert
  actualOutput
      .Should()
      .BeEquivalentTo(expectedOutput);
}


public class Entity
{
  HashSet<IReference>? _References;
  public ICollection<IReference> References
  {
    get => _References ??= new HashSet<IReference>();
    set => _References = value?.ToHashSet();
  }
}

public interface IReference
{
  public string? MyProperty { get; set; }
}

public class Reference : IReference
{
  public string? MyProperty { get; set; }
}

Upvotes: 15

Related Questions