我正在构建一个供多个应用程序使用的库。该库访问第 3 方 API,该 API 返回带有非常无用的字段名称的 JSON 响应。我正在尝试在库中设置模型对象,以便在序列化和反序列化期间使用不同的字段名称。
约束条件:
System.Text.Json
从第 3 方 API 返回的示例 JSON:
{
"FID": 0,
"CTRY22CD": "S92000003",
"CTRY22NM": "Scotland"
}
消费应用程序序列化模型时我想要的示例 JSON:
{
Id: 0,
CountryCode: "S92000003",
CountryName: "Scotland"
}
代表这些 JSON 结构的示例模型 POCO:
using System;
using System.Text.Json.Serialization;
using MyLibrary.Model.Serialization;
namespace MyLibrary.Model;
[UseAsymmetricPropertyNames]
public readonly record struct Country
{
[JsonPropertyName("FID")]
public long Id { get; init; }
[JsonPropertyName("CTRY22CD")]
public required string CountryCode { get; init; }
[JsonPropertyName("CTRY22NM")]
public required string CountryName { get; init; }
}
类上的
UseAsymmetricPropertyNames
属性是一个普通的属性,用于指示这个模型对象应该使用这种特殊的序列化/反序列化语义,它看起来像这样:
using System;
namespace MyLibrary.Model.Serialization;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UseAsymmetricPropertyNamesAttribute : Attribute
{
}
我有一个
JsonConverterFactory
,JsonConverter
和JsonNamingPolicy
定义如下:
JsonConverterFactory
:
using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MyLibrary.Model.Serialization;
public class AsymmetricJsonNamingConverterFactory : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert.GetCustomAttribute<UseAsymmetricPropertyNamesAttribute>() != null;
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return (JsonConverter)Activator.CreateInstance(
typeof(AsymmetricJsonNamingConverter<>).MakeGenericType(typeToConvert),
new AsymmetricJsonNamingPolicy())!;
}
}
JsonConverter
:
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MyLibrary.Model.Serialization;
public class AsymmetricJsonNamingConverter<T> : JsonConverter<T>
{
private readonly JsonNamingPolicy _namingPolicy;
public AsymmetricJsonNamingConverter(JsonNamingPolicy namingPolicy)
{
this._namingPolicy = namingPolicy;
}
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Deserialize using the default naming policy
return JsonSerializer.Deserialize<T>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// Serialize using the provided naming policy
options.PropertyNamingPolicy = this._namingPolicy;
JsonSerializer.Serialize(writer, value, options);
}
}
JsonNamingPolicy
:
using System.Text.Json;
namespace MyLibrary.Model.Serialization;
public class AsymmetricJsonNamingPolicy : JsonNamingPolicy
{
public override string ConvertName(string name)
{
// Use the property name as is during serialization
return name;
}
}
为了使所有消费应用程序的设置相对简单,我在
IServiceCollection
上还有一个扩展方法,用于设置库的依赖注入、验证和其他要求,这还包括 JsonSerializerOptions
的设置:
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MyLibrary.Model.Serialization;
namespace MyLibrary.Extensions;
public static class IServiceCollectionMyLibraryExtensions
{
public static IServiceCollection AddMyLibraryConnector(this IServiceCollection services)
{
// ......
services.Configure<JsonSerializerOptions>((options) =>
{
options.Converters.Add(new AsymmetricJsonNamingConverterFactory());
options.WriteIndented = true;
});
// ......
return services;
}
}
消费应用程序在构建应用程序时在
ConfigureServices()
中调用此扩展方法。
目的是库中注释
UseAsymmetricPropertyNames
的模型对象应该使用它们的属性名称反序列化第三方JSON,但是消费应用程序应该在以后序列化时看到模型对象的属性名称。
我看到的是在调用
JsonConverterFactory
期间添加到 JsonSerializerOptions
的 AddMyLibraryConnector()
被忽略了。
我基本上是在尝试让消费应用程序对第三方属性名称视而不见,同时避免消费应用程序需要在每次序列化调用时传递自定义
JsonSerializationOptions
对象。
我肯定不是第一个需要一个相对简单的方法来解决这个问题的人吧?考虑到初始约束(特别是避免创建重复模型对象的requirement),我缺少什么魔力来使库能够自我设置以确保消费应用程序对第三方字段名称视而不见?
typeInfo 修饰符 自定义类型的contract,而不是转换器,方法是使用自定义属性指定名称而不是标准JsonPropertyNameAttribute
[System.AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true)]
public abstract class JsonAlternativeNameAttributeBase : System.Attribute
{
public JsonAlternativeNameAttributeBase(string? name) => this.Name = name;
public string? Name { get; private set; } // Use null to fall back to member name.
}
public sealed class Json3rdPartyAlternativeNameAttribute : JsonAlternativeNameAttributeBase
{
public Json3rdPartyAlternativeNameAttribute(string name) : base(name) {}
}
public static partial class JsonExtensions
{
public static Action<JsonTypeInfo> UseAternameNames<TAttribute>() where TAttribute : JsonAlternativeNameAttributeBase =>
static typeInfo =>
{
if (typeInfo.Kind != JsonTypeInfoKind.Object)
return;
foreach (var property in typeInfo.Properties)
{
if (property.AttributeProvider?.GetCustomAttributes(typeof(TAttribute), true) is {} list && list.Length > 0)
property.Name = list.OfType<TAttribute>().FirstOrDefault()?.Name ?? property.GetMemberName() ?? property.Name;
}
};
public static string? GetMemberName(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo)?.Name;
}
这里 JsonAlternativeNameAttributeBase
是一个基类,当派生自该基类时,可用于指定备用名称,而
Json3rdPartyAlternativeNameAttribute
是用于此特定第 3 方 API 的具体实现。接下来,修改您的
Country
结构如下:
public readonly record struct Country
{
[Json3rdPartyAlternativeName("FID")]
public long Id { get; init; }
[Json3rdPartyAlternativeName("CTRY22CD")]
public required string CountryCode { get; init; }
[Json3rdPartyAlternativeName("CTRY22NM")]
public required string CountryName { get; init; }
}
然后最后使用不同的选项反序列化和重新序列化,例如:
var inputJson =
"""
{
"FID": 0,
"CTRY22CD": "S92000003",
"CTRY22NM": "Scotland"
}
""";
// Deserialize using altername names.
var inputOptions = new JsonSerializerOptions
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
Modifiers = { JsonExtensions.UseAternameNames<Json3rdPartyAlternativeNameAttribute>() },
},
};
var model = JsonSerializer.Deserialize<Country>(inputJson, inputOptions);
// Re-serialize using standard names.
var outputOptions = new JsonSerializerOptions
{
// Use your standard options here, e.g.:
WriteIndented = true,
};
var outputJson = JsonSerializer.Serialize(model, outputOptions);
根据需要生成:
{
"Id": 0,
"CountryCode": "S92000003",
"CountryName": "Scotland"
}
备注:
AsymmetricJsonNamingConverter<T>
不起作用的原因是您试图通过设置
[JsonPropertyName("FID")]
来覆盖 PropertyNamingPolicy
元数据,但是文档 states
该策略不用于应用了 JsonPropertyNameAttribute 的属性。所以那行不通。要覆盖
[JsonPropertyName]
,您必须使用自定义合约,或者自己在转换器中读取和写入属性(可能使用反射)。
JsonConverter<T>.Read()
或
Write()
中,您不应该修改传入的选项,以防它们在其他线程中使用。相反,使用copy constructor 克隆它们并修改副本。
这里.