【问题标题】:Serialize objects implementing interface with System.Text.Json使用 System.Text.Json 序列化实现接口的对象
【发布时间】:2019-10-14 09:32:35
【问题描述】:

我有一个包含通用集合的大师班。集合中的元素具有不同的类型,并且每个都实现了一个接口。

大师班:

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

元素合同:

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

元素的两个样本:

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; }
}

我需要使用 Json 中的新 System.Text.Json 库来序列化 MasterClass 对象的实例。使用以下代码,

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

我得到以下 JSON:

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

代替:

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

我应该实现哪个类(converter、writer、...)来获取完整的 JSON?

提前感谢您的帮助。

【问题讨论】:

    标签: json .net-core-3.0 system.text.json


    【解决方案1】:

    这对我有用:

    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);
    }
    

    用法:

    var options =
       new JsonSerializerOptions 
       {
         Converters = 
         {
           new TypeMappingConverter<BaseType, ImplementationType>() 
         }
       };
    
    JsonSerializer.Deserialize<Wrapper>(value, options);
    

    测试:

    [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; }
    }
    

    【讨论】:

    • 您的解决方案对我有用,但是是否可以使用在配置 ioc 时已经放置的映射而不是复制它们?
    • @YoyoS 您可以使用 IoC 控制的 JSON 设置注册这些转换器。
    • 你可以更清楚吗?例如,我已经像这样注册了 private void ConfigureServices(ServiceCollection services) { services.AddScoped(); services.AddScoped(); } 避免重复使用相同的 Converters = { new TypeMappingConverter(), new TypeMappingConverter(), }
    • 我在 .NET5 和您的反序列化实现中出现了这种错误。 System.InvalidOperationException:“Lib.Parent”类型的构造函数“Void .ctor(Lib.ICildA,Lib.ICildB)”中的每个参数必须绑定到反序列化时的对象属性或字段。每个参数名称必须与对象上的属性或字段匹配。匹配可以不区分大小写。'
    • 在您的示例中,您有 obe 实现类型,但在问题中有两种类型,ElementAElementB - 我们如何创建一个转换器来确定基于 JSON 的实现它是尝试转换?
    【解决方案2】:

    您要查找的内容称为多态序列化

    Here's Microsoft documentation article

    Here's another question about it

    根据文档,您只需要将接口转换为对象。 例如:

    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];
    }
    

    【讨论】:

      【解决方案3】:

      解决方案是实现一个通用转换器(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}");
          }
      }
      

      这只是Read 方法需要做更多的工作。

      【讨论】:

      【解决方案4】:

      我也遇到过同样的问题,但我的问题可能与您的问题无关。事实证明,传入的 JSON 数据必须序列化到的每个对象都需要一个不带参数的构造函数。我所有的对象都有带有所有参数的构造函数(以便更容易从数据库中创建和填充它们)。

      【讨论】:

      • 您是否尝试添加另一个构造函数 whitout 参数?
      【解决方案5】:

      我目前在 Blazor 应用程序中遇到了同样的问题,所以我无法轻松切换到 Newtonsoft.Json。我找到了两种方法。一种是现实中的hack。您可以创建自定义转换器,在读/写方法中使用Newtonsoft.Json,而不是System.Text.Json。但这不是我想要的。所以我做了一些自定义界面转换器。我有一些可行的解决方案,尚未经过广泛测试,但它可以满足我的需要。

      情况

      我有一个List&lt;TInterface&gt;,其中的对象实现了TInterface。但是有很多不同的实现。我需要在服务器上序列化数据,并在客户端 WASM 应用程序上反序列化所有数据。对于 JavaScript 反序列化,后面提到的自定义 Write 方法的实现就足够了。对于 C# 中的反序列化,我需要知道为列表中的每个项目序列化的对象的确切类型。

      首先,我需要在界面上使用JsonConverterAttribute。所以我关注了这篇文章:https://khalidabuhakmeh.com/serialize-interface-instances-system-text-jsonWriter 的一些实现将处理接口类型。但是没有Read 实现。所以我必须自己做。

      如何

      • 修改Write方法将对象类型作为第一个属性写入JSON对象。使用 JsonDocument 从原始对象中获取所有属性。
      • 读取 JSON 时,使用克隆阅读器(如 Microsoft docs 中针对自定义 json 转换器的建议)查找名为 $type 的第一个属性和类型信息。比创建该类型的实例并使用类型来反序列化来自原始阅读器的数据。

      代码

      接口和类:

      [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; }
      }
      

      接口转换器属性:

      // 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)
          {
          }
      }
      

      转换器:

      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;
                      }
              }
          }
      }
      

      用法:

          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"
          //   }
          // ]
      

      编辑: 我用这个逻辑制作了一个 NuGet 包。你可以在这里下载:InterfaceConverter.SystemTextJson

      【讨论】:

      • 您的解决方案很好,但如果在其他程序集中具体键入呢?
      【解决方案6】:

      改进了@t00thy 解决方案

      您的解决方案很好,但如果在其他程序集中使用具体类型怎么办?

      转换器类

      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;
                      }
              }
          }
      }
      

      转换器属性

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

      接口和类

      [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; }
      }
      

      用法:

              //           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);
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 1970-01-01
      • 2020-04-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-14
      相关资源
      最近更新 更多