英文:
Asymmetric Field Names for serialisation and deserialisation using System.Text.Json
问题
你的问题是如何设置.NET库,以便在序列化和反序列化过程中使用不同的字段名称,同时避免在不同应用程序中创建重复的模型对象。你已经定义了一些相关的类和属性,但似乎JsonConverterFactory
未被正确应用。要解决这个问题,你可以尝试以下步骤:
-
确保
AddMyLibraryConnector()
方法正确调用,并且在应用程序的ConfigureServices()
方法中使用了该扩展方法。 -
确保在
AddMyLibraryConnector()
方法中正确配置了JsonSerializerOptions
。检查options.Converters.Add(new AsymmetricJsonNamingConverterFactory());
是否被添加,并且没有被其他配置覆盖。 -
请确保
UseAsymmetricPropertyNamesAttribute
属性正确应用于需要不同字段名称的模型对象。检查模型是否有[UseAsymmetricPropertyNames]
属性。 -
检查模型类是否位于正确的命名空间,并且程序集加载正确。
-
如果以上步骤都正确,但问题仍然存在,尝试在应用程序中清除缓存,以确保新的配置生效。
如果所有这些步骤都正确实施,但问题仍然存在,可能需要进一步检查库和应用程序的代码以查找可能导致配置未应用的问题。希望这些步骤可以帮助你解决问题。
英文:
I'm building a library to be consumed by several applications. The library accesses a 3rd party API which returns JSON responses with very unhelpful field names. I'm trying to set up the model objects in the library so that different field names are used during serialisation and deserialisation.
Constraints:
- .NET 7+
System.Text.Json
- Use the 3rd party field names within the library when deserialising the response
- Use the library field names when serialising in any consuming application
- Must avoid any need to create duplicate model objects for serialisation and deserialisation
Example JSON returned from the 3rd party API:
{
"FID": 0,
"CTRY22CD": "S92000003",
"CTRY22NM": "Scotland"
}
Example JSON I want when the consuming application serialises the model:
{
Id: 0,
CountryCode: "S92000003",
CountryName: "Scotland"
}
Example Model POCO representing these JSON structures:
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; }
}
The UseAsymmetricPropertyNames
Attribute on the class is a trivial attribute used to indicate that this model object should use this special serialisation / deserialisation semantics, it looks like this:
using System;
namespace MyLibrary.Model.Serialization;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
public sealed class UseAsymmetricPropertyNamesAttribute : Attribute
{
}
I have a JsonConverterFactory
, JsonConverter
and JsonNamingPolicy
defined as follows:
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;
}
}
To make setup in all consuming applications relatively trivial, I also have an extension method on IServiceCollection
which is used to set up dependency injection, validation and other requirements for the library, this also includes setup for 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;
}
}
The consuming application calls this extension method in ConfigureServices()
while building the application.
The intent is that model objects annotated UseAsymmetricPropertyNames
in the library should deserialise the 3rd party JSON using their property names, but the consuming applications should then see the property names from the model objects when later serialising.
What I'm seeing though is that the JsonConverterFactory
added to the JsonSerializerOptions
during the call to AddMyLibraryConnector()
is ignored.
I'm trying essentially to make the consuming applications blind to the 3rd party property names, and also to avoid the consuming application needing to pass a custom JsonSerializationOptions
object on every serialisation call.
Surely I can't be the first to have needed a relatively simple way to solve this problem? What magic am I missing to enable the library to set itself up to ensure that the consuming applications are blind to the 3rd party field names, taking into account the initial constraints (particularly the requirement to avoid creating duplicate model objects)?
答案1
得分: 2
以下是您要求的翻译内容:
与其使用转换器,您可以使用typeInfo modifier来自定义从第三方API反序列化JSON时的类型contract,通过使用自定义属性来指定名称,而不是标准的JsonPropertyNameAttribute
。
按照这种方法,定义以下属性和typeinfo modifier:
[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
是一个用于特定第三方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);
这将生成所需的JSON输出:
{
"Id": 0,
"CountryCode": "S92000003",
"CountryName": "Scotland"
}
请注意:
-
您的
AsymmetricJsonNamingConverter<T>
不起作用的原因是,您试图通过设置PropertyNamingPolicy
来覆盖[JsonPropertyName("FID")]
元数据,然而文档说明:该策略不适用于已应用JsonPropertyNameAttribute的属性。
因此,这不会起作用。要覆盖
[JsonPropertyName]
,您必须使用自定义的contract,或者在转换器内部自己读取和写入属性(可能使用反射)。 -
顺便说一下,在
JsonConverter<T>.Read()
或Write()
内部,不应该修改传入的选项,以防它们在其他线程中使用。相反,使用复制构造函数克隆它们并修改副本。
英文:
Rather than a converter, you could use a typeInfo modifier to customize your type's contract when deserializing JSON from 3rd party API, by using a custom attribute to specify the name rather than the standard JsonPropertyNameAttribute
.
Following this approach, define the following attributes and typeinfo modifier:
[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;
}
Here JsonAlternativeNameAttributeBase
is a base class that, when derived from, can be used to specify an alternate name, and Json3rdPartyAlternativeNameAttribute
is a concrete implementation to use for this specific 3rd party API.
Next, modify your Country
struct as follows:
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; }
}
Then finally deserialize and re-serialize using different options e.g.:
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);
Which generates, as required:
{
"Id": 0,
"CountryCode": "S92000003",
"CountryName": "Scotland"
}
Notes:
-
The reason your
AsymmetricJsonNamingConverter<T>
does not work is that you are trying to override[JsonPropertyName("FID")]
metadata by setting aPropertyNamingPolicy
, however the documentation states> The policy is not used for properties that have a JsonPropertyNameAttribute applied.
So that won't work. To override
[JsonPropertyName]
you must use a customized contract, or read & write the properties yourself inside the converter (possibly using reflection). -
Incidentally, inside
JsonConverter<T>.Read()
orWrite()
you should not modify the incoming options, in case they are being used in some other thread. Instead, clone them with the copy constructor and modify the copy.
Demo fiddle here.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论