Json.NET - 根据值改变架构/合同的正确方法吗?

时间:2018-06-14 08:33:37

标签: json.net

我正在将JSON序列化以包含在搜索索引(Elasticsearch)中。

我正在序列化的一个属性是记录器状态,它可以包含任何对象。我限制了递归的深度并处理序列化错误。然而,我遇到的问题是,不同的对象/类型可能具有相同但具有不同类型的成员。

例如,我可能会将{ scope: { query: "?hello=true" } }序列化为一种类型,将{ scope: { query: { userId: 123 } } }序列化为另一种类型。

Elasticsearch不喜欢这样,所以我的想法是,对于用户定义的结果对象的部分,我将使用不同的属性名称,因此它们各自获得自己的索引字段,对应于它们的类型。

我正在考虑将上述两个例子翻译成:

{ scope: { query_str: "?hello=true" } }{ scope: { query_obj: { userId_num: 123 } } }

{ scope: { query: { str: "?hello=true" } } }{ scope: { query: { obj: { userId: { num: 123 } } } } }

几乎可以肯定前者..但是,在Json.NET内部似乎没有办法根据运行时的属性值改变属性名称。

我无法覆盖内部序列化编写器的对象和字典序列化,似乎没有一个扩展点来执行此操作。

我想到的方法:

  1. 使用反射预先构建嵌套字典 - 繁琐复杂,并且不会做Json.NET在数据合同,动态等方面所做的聪明工作。
  2. 创建一个包裹JsonTextWriter,查看Path等以使更改处于较低级别
  3. 在初始序列化后,反序列化,重新组织并再次序列化
  4. 似乎没什么好看的。什么是我最好的选择?

    编辑:测试用例 -

        public class A
        {
            public int Num { get; set; }
        }
    
        [Fact]
        public void JsonFieldNamesMangled()
        {
            var entry = new Dictionary<string, object>()
            {
                { "abc", "def" },
                {
                    "scope", new
                    {
                        SomeString1 = "hello",
                        SomeInt1 = 12,
                        SomeArr1 = new[] { 1, 2 }
                    }
                }
            };
            entry["state"] = new Dictionary<string, object>
            {
                { "SomeString2", "goodbye" },
                { "SomeInt2", 34 },
                { "SomeArr2", new[] { new A { Num = 1 }, new A { Num = 2 } } }
            };
    
            var json = JsonConvert.SerializeObject(entry);
            // original: "{\"abc\":\"def\",\"scope\":{\"SomeString1\":\"hello\",\"SomeInt1\":12,\"SomeArr1\":[1,2]},\"state\":{\"SomeString2\":\"goodbye\",\"SomeInt2\":34,\"SomeArr2\":[{\"Num\":1},{\"Num\":2}]}}");
            json.Should().Be("{\"abc\":\"def\",\"scope\":{\"SomeString1_str\":\"hello\",\"SomeInt1_num\":12,\"SomeArr1_arr_num\":[1,2]},\"state\":{\"SomeString2_str\":\"goodbye\",\"SomeInt2_num\":34,\"SomeArr2_arr_obj\":[{\"Num_num\":1},{\"Num_num\":2}]}}");
        }
    

1 个答案:

答案 0 :(得分:1)

一个选项是实现自己的custom JsonConverter,其WriteJson()方法重新实现JsonSerializerInternalWriter.SerializeObject()逻辑的一些最小必要子集,同时根据属性值重新映射属性名称类型:

public abstract class JsonObjectPropertyNameRemappingConverterBase : JsonConverter
{
    public override bool CanRead { get { return false; } }

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

    protected abstract string ResolvePropertyName(JsonSerializer serializer, JsonProperty property, object value);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());

        writer.WriteStartObject();
        foreach (var property in contract.Properties)
        {
            if (!property.Readable || property.Ignored || (property.ShouldSerialize != null && !property.ShouldSerialize(value)))
                continue;
            var propertyValue = property.ValueProvider.GetValue(value);
            if (propertyValue == null && serializer.NullValueHandling == NullValueHandling.Ignore)
                continue;
            //Todo if required:
            //serializer.DefaultValueHandling, serializer.PreserveReferencesHandling, serializer.ReferenceLoopHandling, serializer.TypeNameHandling
            //property.Converter, property.ItemConverter
            var name = ResolvePropertyName(serializer, property, propertyValue);
            writer.WritePropertyName(name);
            serializer.Serialize(writer, propertyValue);
        }
        writer.WriteEndObject();
    }
}

public class JsonObjectPropertyNameRemappingConverter : JsonObjectPropertyNameRemappingConverterBase
{
    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException("This converter should be applied via [JsonConverter(typeof(JsonObjectPropertyNameRemappingConverter)]");
    }

    protected override string ResolvePropertyName(JsonSerializer serializer, JsonProperty property, object value)
    {
        // Replace with whatever logic you want here.
        if (value == null)
            return property.PropertyName;
        var contract = serializer.ContractResolver.ResolveContract(value.GetType());
        string type;
        if (contract is JsonObjectContract)
            type = "obj";
        else if (contract is JsonArrayContract)
            type = "array";
        else if (value is string)
            type = "str";
        else if (value is Int32)
            type = "num";
        else
            type = value.GetType().Name.ToLowerInvariant();
        return property.PropertyName + "_" + type;
    }
}

public class JsonObjectPropertyNameRemappingConverter<T> : JsonObjectPropertyNameRemappingConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }
}

然后你就可以将它应用到你的模型中:

[JsonConverter(typeof(JsonObjectPropertyNameRemappingConverter))]
public class Scope
{
    public object Query { get; set; }
}

public class RootObject
{
    public Scope Scope { get; set; }
}

[JsonConverter(typeof(JsonObjectPropertyNameRemappingConverter))]
class Id
{
    public int UserId { get; set; }
}

或者在如此设置中应用它:

var settings = new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    NullValueHandling = NullValueHandling.Ignore,
    Converters = { new JsonObjectPropertyNameRemappingConverter<Scope>(), new JsonObjectPropertyNameRemappingConverter<Id>() },
};
var json = JsonConvert.SerializeObject(root, Formatting.Indented, settings);

注意:

  • 转换器实现选项#1。

  • 转换器使用Json.NET的contract resolver来执行所有必要的反射。这简化了逻辑,自动支持骆驼外壳,并且应该通过缓存反射元数据来提高性能。

  • 转换器不支持实现ISerializable的词典和类型。

  • ReadJson()未实施(目前尚不清楚如何实施。)

工作样本.Net小提琴here