3

.NET 在使用REST API时更喜欢 Newtonsoft JSON 序列化器/反序列化器。

D&B Direct REST 实现对 JSON 使用BadgerFish方法(主要存在于 Java 世界(jettison 命名空间)中,但有一些小的变化:D&B BadgerFish

我想将 D&B BadgerFish JSON 响应映射到 .NET 类。有一个GitHub项目https://github.com/bramstein/xsltjson/可以实现从 XML 到 JSON 的转换(支持 BadgerFish),但是我该如何做相反的事情,如下所述:

XSLTJSON 支持几种不同的 JSON 输出格式,从紧凑的输出格式到支持 BadgerFish 约定,它允许 XML 和 JSON 之间的往返。

例如,假设 D&B 后端 REST 服务正在转换此 XML:

<SalesRevenueAmount CurrencyISOAlpha3Code="USD”&gt;1000000</SalesRevenueAmount>
<SalesRevenueAmount CurrencyISOAlpha3Code="CAD”&gt;1040000</SalesRevenueAmount>

.. 进入:

"SalesRevenueAmount": [     {
   "@CurrencyISOAlpha3Code": "USD",
   "$": 1000000
},
{
   "@CurrencyISOAlpha3Code": "CAD",
   "$": 1040000
}
]

那么如何在 .NET REST 客户端中使用这个返回 BadgerFish 格式的 JSON 响应(对原始规范稍作修改)?

4

1 回答 1

1

我也负责使用 D&B 的 API,并且在检查 .NET 中是否存在适用于 BadgerFish 的现有解决方案时遇到了这个问题。

和你一样,我只需要担心反序列化到我的 .NET 模型中。

此外,在阅读了 D&B 的 BadgerFish 变体之后,我认为没有必要专门解释它们。以下代码似乎可以很好地处理 D&B 的格式。

为什么是獾鱼?

似乎 D&B 拥有 XML API 已经有一段时间了,他们决定通过将现有 XML 直接转换为 JSON 来生成 JSON 内容类型,而不是序列化为 XMLJSON。

这导致需要解决 XML 和 JSON 结构之间的不一致问题。在 XML 中,您可以拥有与单个元素关联的属性和值。JSON 中不存在该范式。JSON 只是键/值。

因此,BadgerFish是一个旨在解决两种数据格式之间不一致的标准。当然,它可以通过其他方式解决,这只是众多想法之一。

目标

为了解决这个问题,我首先需要弄清楚我的预期结果是什么。

使用您的示例,我决定使用以下 JSON:

"SalesRevenueAmount": [
    {
       "@CurrencyISOAlpha3Code": "USD",
       "$": 1000000
    },
    {
       "@CurrencyISOAlpha3Code": "CAD",
       "$": 1040000
    }
]

应反序列化为模型集合,如下所示:

public class SalesRevenueAmount {
    public string CurrencyISOAlpha3Code { get; set; }
    public string Value { get; set; }
}

最简单的解决方案

最简单且最明显的解决方案是将JsonProperty属性附加到我希望具有此@$命名约定的每个属性。

public class SalesRevenueAmount {
    [JsonProperty("@CurrencyISOAlpha3Code")]
    public string CurrencyISOAlpha3Code { get; set; }
    [JsonProperty("$")]
    public string Value { get; set; }
}

这样做比较简单,但也极容易出错。如果可以避免的话,我也不喜欢像这样将基础设施层的特定属性附加到我的模型中。

更好的解决方案

因此,我推测一个更好的解决方案将是我不必被迫维护和手写这些容易出错的注释。当然,我仍然必须自己编写属性名称,但可以在 Visual Studio 或您喜欢的任何 IDE 中轻松重构这些名称。另一方面,在运行时或单元测试失败之前,不会捕获属性中的魔术字符串。

因此,我想要一些更自动化、更强大和更干燥的东西。在深入研究了 Newtonsoft JSON 之后,我终于想出了一个我很满意的解决方案。我创建了一个简单JsonConverter的我正在调用BadgerFishJsonConverter的 .

当前的实现只处理反序列化,但调整它来进行序列化并不难。我只是还没有这个需要。如果我将来这样做,我会回来更新我的答案。

public class BadgerFishJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return true;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var source = JObject.Load(reader);

        //Since we can't modify the internal collections, first we will get all the paths.
        //Then we will proceed to rename them.
        var paths = new List<string>();
        collectPaths(source, paths);
        renameProperties(source, paths);

        return source.ToObject(objectType);
    }

    private void collectPaths(JToken token, ICollection<string> collection)
    {
        switch (token.Type)
        {
            case JTokenType.Object:
            case JTokenType.Array:
                foreach (var child in token)
                {
                    collectPaths(child, collection);
                }
                break;
            case JTokenType.Property:
                var property = (JProperty)token;

                if (shouldRenameProperty(property.Name))
                {
                    collection.Add(property.Path);
                }

                foreach (var child in property)
                {
                    collectPaths(child, collection);
                }
                break;
            default:
                break;
        }
    }

    private void renameProperties(JObject source, ICollection<string> paths)
    {
        foreach (var path in paths)
        {
            var token = source.SelectToken(path);
            token.Rename(prop => transformPropertyName(prop));
        }
    }

    private bool shouldRenameProperty(string propertyName)
    {
        return propertyName.StartsWith("@") || propertyName.Equals("$");
    }

    private static string transformPropertyName(JProperty property)
    {
        if (property.Name.StartsWith("@"))
        {
            return property.Name.Substring(1);
        }
        else if (property.Name.Equals("$"))
        {
            return "Value";
        }
        else
        {
            return property.Name;
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

如果我想在这方面花更多时间,它肯定会写得更高,但我的项目根本不需要那种速度。

ReadJson它当前使用JObject.Load(reader)的将 JSON 转换为as的JObject方法使用默认实现。

然后,我在该对象的图形上递归,收集我想要重命名的属性的路径。这是因为我无法在枚举期间重命名它们,因为这会修改正在迭代的集合,这显然是不允许的。

收集路径后,我迭代路径,重命名这些特定属性。此过程首先删除旧属性,然后添加具有新名称的新属性。

JsonReader对于那些如此倾向于的人,一个更精明和有效的实现将在构建的反序列化阶段完成所有这些工作,并在JObject从读者那里读取属性时重命名它们。

用法

用法很简单,如下:

var jsonSettings = new JsonSerializerSettings();
jsonSettings.Converters.Add(new BadgerFishJsonConverter());

var obj = JsonConvert.DeserializeObject<SalesRevenueAmounts>(json, jsonSettings); 

给定以下两个模型:

public class SalesRevenueAmount
{
    public string CurrencyISOAlpha3Code { get; set; }
    public string Value { get; set; }
}

public class SalesRevenueAmounts
{
    public IEnumerable<SalesRevenueAmount> SalesRevenueAmount { get; set; }
}

附加参考

作为我的解决方案的一部分,我使用了来自用户Brian Rogers的这个重命名扩展,我发现它有助于整理我的代码。我添加了通过简单地将参数更改为 a 来传递名称提供程序函数的功能,以便我可以控制提供程序名称的创建方式。Func<JProperty, string>

完整实现,如下:

public static class Extensions
{
    public static void Rename(this JToken token, string newName)
    {
        token.Rename(prop => newName);
    }

    public static void Rename(this JToken token, Func<JProperty, string> nameProvider)
    {
        if (token == null)
            throw new ArgumentNullException("token", "Cannot rename a null token");

        JProperty property;

        if (token.Type == JTokenType.Property)
        {
            if (token.Parent == null)
                throw new InvalidOperationException("Cannot rename a property with no parent");

            property = (JProperty)token;
        }
        else
        {
            if (token.Parent == null || token.Parent.Type != JTokenType.Property)
                throw new InvalidOperationException("This token's parent is not a JProperty; cannot rename");

            property = (JProperty)token.Parent;
        }

        var newName = nameProvider.Invoke(property);
        var newProperty = new JProperty(newName, property.Value);
        property.Replace(newProperty);
    }
}

希望这有助于将来节省某人的时间。

于 2019-04-12T21:04:31.997 回答