【问题标题】:Serialize single KeyValuePair<> to JSON as part of a parent object将单个 KeyValuePair<> 序列化为 JSON 作为父对象的一部分
【发布时间】: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 属性但可以添加自己的自定义属性吗?

标签: c# json asp.net-web-api json.net


【解决方案1】:

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

public class MyDto
{
    [JsonExtensionData]
    public Dictionary<string, object> Identifier { get; set; }

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

首先,有关使用自定义扩展数据属性和自定义JsonConverter 的示例,请参阅JsonTypedExtensionDatathis answerHow 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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-08-15
      • 1970-01-01
      • 2017-05-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-03-01
      相关资源
      最近更新 更多