“使用System.Text.Json进行序列化和反序列化的不对称字段名称”

huangapple go评论72阅读模式
英文:

Asymmetric Field Names for serialisation and deserialisation using System.Text.Json

问题

你的问题是如何设置.NET库,以便在序列化和反序列化过程中使用不同的字段名称,同时避免在不同应用程序中创建重复的模型对象。你已经定义了一些相关的类和属性,但似乎JsonConverterFactory未被正确应用。要解决这个问题,你可以尝试以下步骤:

  1. 确保AddMyLibraryConnector()方法正确调用,并且在应用程序的ConfigureServices()方法中使用了该扩展方法。

  2. 确保在AddMyLibraryConnector()方法中正确配置了JsonSerializerOptions。检查options.Converters.Add(new AsymmetricJsonNamingConverterFactory());是否被添加,并且没有被其他配置覆盖。

  3. 请确保UseAsymmetricPropertyNamesAttribute属性正确应用于需要不同字段名称的模型对象。检查模型是否有[UseAsymmetricPropertyNames]属性。

  4. 检查模型类是否位于正确的命名空间,并且程序集加载正确。

  5. 如果以上步骤都正确,但问题仍然存在,尝试在应用程序中清除缓存,以确保新的配置生效。

如果所有这些步骤都正确实施,但问题仍然存在,可能需要进一步检查库和应用程序的代码以查找可能导致配置未应用的问题。希望这些步骤可以帮助你解决问题。

英文:

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) =&gt; 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&lt;JsonTypeInfo&gt; UseAternameNames&lt;TAttribute&gt;() where TAttribute : JsonAlternativeNameAttributeBase =&gt;
		static typeInfo =&gt;
		{
			if (typeInfo.Kind != JsonTypeInfoKind.Object)
				return;
			foreach (var property in typeInfo.Properties)
			{
				if (property.AttributeProvider?.GetCustomAttributes(typeof(TAttribute), true) is {} list &amp;&amp; list.Length &gt; 0)
					property.Name = list.OfType&lt;TAttribute&gt;().FirstOrDefault()?.Name ?? property.GetMemberName() ?? property.Name;
			}
		};

	public static string? GetMemberName(this JsonPropertyInfo property) =&gt; (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(&quot;FID&quot;)]
  public long Id { get; init; }

  [Json3rdPartyAlternativeName(&quot;CTRY22CD&quot;)]
  public required string CountryCode { get; init; }

  [Json3rdPartyAlternativeName(&quot;CTRY22NM&quot;)]
  public required string CountryName { get; init; }
}

Then finally deserialize and re-serialize using different options e.g.:

var inputJson = 			
	&quot;&quot;&quot;
	{
	  &quot;FID&quot;: 0,
	  &quot;CTRY22CD&quot;: &quot;S92000003&quot;,
	  &quot;CTRY22NM&quot;: &quot;Scotland&quot;
	}
	&quot;&quot;&quot;;			

// Deserialize using altername names.
var inputOptions = new JsonSerializerOptions
{
	TypeInfoResolver = new DefaultJsonTypeInfoResolver
	{
		Modifiers = { JsonExtensions.UseAternameNames&lt;Json3rdPartyAlternativeNameAttribute&gt;() },
	},
};

var model = JsonSerializer.Deserialize&lt;Country&gt;(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:

{
  &quot;Id&quot;: 0,
  &quot;CountryCode&quot;: &quot;S92000003&quot;,
  &quot;CountryName&quot;: &quot;Scotland&quot;
}

Notes:

  • The reason your AsymmetricJsonNamingConverter&lt;T&gt; does not work is that you are trying to override [JsonPropertyName(&quot;FID&quot;)] metadata by setting a PropertyNamingPolicy, 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&lt;T&gt;.Read() or Write() 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.

huangapple
  • 本文由 发表于 2023年5月15日 07:54:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/76250152.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定