如何在 C# 中将 Badgerfish 样式的 JSON 转换为 .NET 对象或 XML

本文关键字:转换 NET 对象 XML JSON 中将 样式 Badgerfish | 更新日期: 2023-09-27 18:03:44

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

D&B Direct REST实现使用BadgerFish方法(主要存在于Java世界(抛弃命名空间(中(,用于JSON,有一些细微的变化: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”>1000000</SalesRevenueAmount>
<SalesRevenueAmount CurrencyISOAlpha3Code="CAD”>1040000</SalesRevenueAmount>

。。到:

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

那么,如何在 .NET REST 客户端中使用这个返回的 BadgerFish 格式的 JSON 响应(从原始规范略有修改(呢?

如何在 C# 中将 Badgerfish 样式的 JSON 转换为 .NET 对象或 XML

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

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

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

为什么选择獾鱼?

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

这导致需要解决 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 转换为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的这个重命名扩展,我发现这有助于整理我的代码。我添加了传入名称提供程序函数的功能,只需将参数更改为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);
    }
}

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