将单个 KeyValuePair<> 序列化为 JSON 作为父对象的一部分

Posted

技术标签:

【中文标题】将单个 KeyValuePair<> 序列化为 JSON 作为父对象的一部分【英文标题】:Serialize single KeyValuePair<> to JSON as part of a parent object 【发布时间】:2020-08-31 01:58:44 【问题描述】:

我有一个这样的 DTO 类:

public class MyDto

    public KeyValuePair<string, string> Identifier  get; set; 

    public double Value1  get; set; 

    public string Value2  get; set; 

两个实例可能如下所示:

var results = new List<MyDto> 

    new MyDto
    
        Identifier = new KeyValuePair<string, string>("Car", "Ford"),
        Value1 = 13,
        Value2 = "A"
    ,
    new MyDto 
    
        Identifier = new KeyValuePair<string, string>("Train", "Bombardier"),
        Value1 = 14,
        Value2 = "B"
    ,
;

在 ASP.NET Web API(使用 Json.NET)中序列化结果时,输出如下所示:

[
  
    "Identifier": 
      "Key": "Car",
      "Value": "Ford"
    ,
    "Value1": 13,
    "Value2": "A"
  ,
  
    "Identifier": 
      "Key": "Train",
      "Value": "Bombardier"
    ,
    "Value1": 14,
    "Value2": "B"
  
]

但我想这样:

[
  
    "Car": "Ford",
    "Value1": 13,
    "Value2": "A"
  ,
  
    "Train": "Bombardier",
    "Value1": 14,
    "Value2": "B"
  
]

我怎样才能做到这一点?我是否必须编写自定义JsonConverter 并手动完成所有操作?由于 DTO 类位于无法访问 Json.NET 的单独程序集中,因此无法使用特定的 Json.NET 属性。

我不一定要使用KeyValuePair&lt;string, string&gt;。我只需要一个允许我拥有灵活属性名称的数据结构。

【问题讨论】:

.NET Core 3+ 中 JSON 的 System.Text.Json 实现目前有很多限制,其中之一是它无法正确序列化 KeyValuePair&lt;TKey, TValue&gt; 属性。目前计划在 .NET 5.0(将于 2020 年 11 月发布)中解决此问题正在此处跟踪:github.com/dotnet/runtime/issues/30524 @silkfire 使用 Json.NET 序列化这两个实例时 我使用的是 .NET Framework,而不是 .NET Core 我必须写一个自定义的 JsonConverter - 他们真的没有那么复杂。但最简单的方法是像 Marc 在他的回答中建议的那样,只使用一个条目字典。 无法添加 Json.NET 属性但可以添加自己的自定义属性吗? 【参考方案1】:

您可以通过应用[JsonExtensionData] 轻松地将字典序列化为父对象的一部分,而不是KeyValuePair&lt;&gt;,如下所示:

public class MyDto

    [JsonExtensionData]
    public Dictionary<string, object> Identifier  get; set; 

但是,您已声明 无法使用特定的 Json.NET 属性。 但是,由于您通常可以修改 DTO,因此您可以使用 自定义扩展数据属性 然后在 custom generic JsonConverter 或 custom contract resolver 中处理该属性。

首先,有关使用自定义扩展数据属性和自定义JsonConverter 的示例,请参阅JsonTypedExtensionData 从this answer 到How to deserialize a child object with dynamic (numeric) key names? p>

其次,如果您不想使用转换器,使用自定义合约解析器处理自定义扩展数据属性,首先定义以下合约解析器:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class MyJsonExtensionDataAttribute : Attribute



public class MyContractResolver : DefaultContractResolver

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    
        var contract = base.CreateObjectContract(objectType);
        if (contract.ExtensionDataGetter == null && contract.ExtensionDataSetter == null)
        
            var dictionaryProperty = contract.Properties
                .Where(p => typeof(IDictionary<string, object>).IsAssignableFrom(p.PropertyType) && p.Readable && p.Writable)
                .Where(p => p.AttributeProvider.GetAttributes(typeof(MyJsonExtensionDataAttribute), false).Any())
                .SingleOrDefault();
            if (dictionaryProperty != null)
            
                dictionaryProperty.Ignored = true;
                contract.ExtensionDataGetter = o => 
                    ((IDictionary<string, object>)dictionaryProperty.ValueProvider.GetValue(o)).Select(p => new KeyValuePair<object, object>(p.Key, p.Value));
                contract.ExtensionDataSetter = (o, key, value) =>
                    
                        var dictionary = (IDictionary<string, object>)dictionaryProperty.ValueProvider.GetValue(o);
                        if (dictionary == null)
                        
                            dictionary = (IDictionary<string, object>)this.ResolveContract(dictionaryProperty.PropertyType).DefaultCreator();
                            dictionaryProperty.ValueProvider.SetValue(o, dictionary);
                        
                        dictionary.Add(key, value);
                    ;
                
                contract.ExtensionDataValueType = typeof(object);
                // TODO set contract.ExtensionDataNameResolver
        
        return contract;
    

然后如下修改你的 DTO:

public class MyDto

    [MyJsonExtensionData]
    public Dictionary<string, object> Identifier  get; set; 

    public double Value1  get; set; 

    public string Value2  get; set; 

并按如下方式进行序列化,缓存解析器的静态实例以提高性能:

static IContractResolver resolver = new MyContractResolver();

// And later

    var settings = new JsonSerializerSettings
    
        ContractResolver = resolver,
    ;

    var json = JsonConvert.SerializeObject(results, Formatting.Indented, settings);

注意事项:

MyJsonExtensionData 属性的类型必须可分配给类型 IDictionary&lt;string, object&gt; 并具有公共的无参数构造函数。

未实现扩展数据属性名称的命名策略。

Json.NET 在每个对象的末尾序列化扩展数据属性,而您的问题在开头显示自定义属性。由于 JSON 对象被 standard 定义为 一组无序的名称/值对,我认为这无关紧要。但是,如果您需要在对象的开头使用自定义属性,则可能需要使用自定义转换器而不是自定义合约解析器。

演示小提琴here.

【讨论】:

【参考方案2】:

编辑:此答案适用于最初提出的问题;它随后被编辑过,现在可能没那么有用了


您可能需要使用具有单个元素的字典,即Dictionary&lt;string,string&gt;,它确实以您想要的方式进行序列化;例如:

var obj = new Dictionary<string, string>   "Car", "Ford"  ;
var json = JsonConvert.SerializeObject(obj);
System.Console.WriteLine(json);

【讨论】:

我试过你的代码,但它序列化为"Identifier": "Car": "Ford" ,而不是 "Car": "Ford" 那是因为Identifier 是您的财产的名称。根据 default 行为,这是正确的。 这可能是真的,但这不是我需要的。 那么你需要一个自定义转换器,就像你自己建议的那样:) @mu88 你会注意到我没有序列化一个对象 with 标识符是字典;我序列化了只是字典【参考方案3】:

由于您的 MyDto 类位于一个单独的程序集中,您可以对其进行的更改种类受到限制,那么是的,我认为您最好的选择是为该类创建一个自定义转换器。像这样的东西应该可以工作:

public class MyDtoConverter : JsonConverter

    public override bool CanConvert(Type objectType)
    
        return objectType == typeof(MyDto);
    

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    
        MyDto dto = (MyDto)value;
        writer.WriteStartObject();
        writer.WritePropertyName(dto.Identifier.Key);
        writer.WriteValue(dto.Identifier.Value);
        var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(typeof(MyDto));
        foreach (JsonProperty prop in contract.Properties.Where(p => p.PropertyName != nameof(MyDto.Identifier)))
        
            writer.WritePropertyName(prop.PropertyName);
            writer.WriteValue(prop.ValueProvider.GetValue(dto));
        
        writer.WriteEndObject();
    

    public override bool CanRead => false;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    
        throw new NotImplementedException();
    

要使用它,您需要将转换器添加到您的 Web API 配置中:

config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new MyDtoConverter());

这是控制台应用程序中转换器的工作演示:https://dotnetfiddle.net/DksgMZ

【讨论】:

以上是关于将单个 KeyValuePair<> 序列化为 JSON 作为父对象的一部分的主要内容,如果未能解决你的问题,请参考以下文章

将 JSON 反序列化为 List<List<KeyValuePair>> 结果为空

从 IEnumerable<KeyValuePair<>> 重新创建字典

从 List<KeyValuePair<string,string>> 返回匹配

List<KeyValuePair<string,string>> 的语法更好?

KeyValuePair的使用

将 KeyValuePair ForEach 转换为 LINQ