System.Text.Json 和动态解析多态对象

System.Text.Json and Dynamically Parsing polymorphic objects

我不相信我正在思考如何在解析 json 结果时正确使用 JsonConverter 实现多态性。

在我的方案中,我的目标是 Git TFS 中的策略配置。策略配置:


"value": [
{
        "createdBy": {
            "displayName": "username",
            "url": "url",
            "id": "id",
            "uniqueName": "user",
            "imageUrl": "url"
        },
        "createdDate": "2020-03-21T18:17:24.3240783Z",
        "isEnabled": true,
        "isBlocking": true,
        "isDeleted": false,
        "settings": {
            "minimumApproverCount": 1,
            "creatorVoteCounts": false,
            "allowDownvotes": false,
            "resetOnSourcePush": true,
            "scope": [{
                    "refName": "refs/heads/master",
                    "matchKind": "Exact",
                    "repositoryId": "id"
                }
            ]
        },
        "_links": {
            "self": {
                "href": "url"
            },
            "policyType": {
                "href": "url"
            }
        },
        "revision": 1,
        "id": 974,
        "url": "url",
        "type": {
            "id": "id",
            "url": "url",
            "displayName": "Minimum number of reviewers"
        },
{...}]

更多settings例子: 需要合并策略

"settings": {
        "useSquashMerge": true,
        "scope": [
            {
                "refName": "refs/heads/master",
                "matchKind": "Exact",
                "repositoryId": "id"
            }
        ]
    }

需要审稿人

    "settings": {
        "requiredReviewerIds": [
            "id"
        ],
        "scope": [
            {
                "refName": "refs/heads/master",
                "matchKind": "Exact",
                "repositoryId": "id"
            }
        ]
    }

在上面的 json 片段中,设置对象因配置类型而异。

除了动态 serialize/deserialize 设置对象之外,编写转换器的最佳方法是什么?我已经阅读了几篇关于此的文章,但无法完全理解它。


这就是我目前反序列化所有 API 结果的方式,到目前为止它们都是简单的结果集。

async Task<List<T>> ParseResults<T>( HttpResponseMessage result, string parameter )
{
    List<T> results = new List<T>();

    if ( result.IsSuccessStatusCode )
    {
        using var stream = await result.Content.ReadAsStreamAsync();
        JsonDocument doc = JsonDocument.Parse( stream );
        JsonElement collection = doc.RootElement.GetProperty( parameter ).Clone();

        foreach ( var item in collection.EnumerateArray() )
        {
            results.Add( JsonSerializer.Deserialize<T>( item.ToString() ) );
        }
    }

    return results;
}

我的集成测试。

PolicyConfiguration 是我要反序列化的类型。

[Test]
public async Task Get_TestMasterBranchPolicyConfigurations()
{
    HttpResponseMessage result = await GetResult( $"{_collection}/ProductionBuildTesting/_apis/policy/configurations?api-version=4.1" );

    List<PolicyConfiguration> configurations = await ParseResults<PolicyConfiguration>( result, "value" );
    Assert.AreEqual( 16, configurations.Count );
    JsonPrint( configurations );
}

对于这种解析情况,我目前 类

public class CreatedBy
{
    [JsonPropertyName( "displayName" )]
    public string DisplayName { get; set; }
    [JsonPropertyName( "url" )]
    public string Url { get; set; }
    [JsonPropertyName( "id" )]
    public Guid Id { get; set; }
    [JsonPropertyName( "uniqueName" )]
    public string UniqueName { get; set; }
    [JsonPropertyName( "imageUrl" )]
    public string ImageUrl { get; set; }
}

public class PolicyConfigurationScope
{
    [JsonPropertyName( "refName" )]
    public string RefName { get; set; }
    [JsonPropertyName( "matchKind" )]
    public string MatchKind { get; set; }
    [JsonPropertyName( "repositoryId" )]
    public Guid RepositoryId { get; set; }
}

public class PolicyConfigurationSettings_MinimumNumberOfReviewers
{
    [JsonPropertyName( "minimumApproverCount" )]
    public int MinimumApproverCount { get; set; }
    [JsonPropertyName( "creatorVoteCounts" )]
    public bool CreatorVoteCounts { get; set; }
    [JsonPropertyName( "allowDownvotes" )]
    public bool AllowDownvotes { get; set; }
    [JsonPropertyName( "resetOnSourcePush" )]
    public bool ResetOnSourcePush { get; set; }
    [JsonPropertyName( "scope" )]
    public List<PolicyConfigurationScope> Scope { get; set; }
}

public class PolicyConfigurationType
{
    [JsonPropertyName( "id" )]
    public Guid Id { get; set; }
    [JsonPropertyName( "url" )]
    public string Url { get; set; }
    [JsonPropertyName( "displayName" )]
    public string DisplayName { get; set; }
}

public class PolicyConfiguration
{
    [JsonPropertyName( "createdBy" )]
    public CreatedBy CreatedBy { get; set; }
    [JsonPropertyName( "createdDate" )]
    public DateTime CreatedDate { get; set; }
    [JsonPropertyName( "isEnabled" )]
    public bool IsEnabled { get; set; }
    [JsonPropertyName( "isBlocking" )]
    public bool IsBlocking { get; set; }
    [JsonPropertyName( "isDeleted" )]
    public bool IsDeleted { get; set; }
    //[JsonPropertyName( "settings" )]
    //public PolicyConfigurationSettings_MinimumNumberOfReviewersSettings Settings { get; set; }
    [JsonPropertyName( "revision" )]
    public int Revision { get; set; }
    [JsonPropertyName( "id" )]
    public int Id { get; set; }
    [JsonPropertyName( "url" )]
    public string Url { get; set; }
    [JsonPropertyName( "type" )]
    public PolicyConfigurationType Type { get; set; }
}

我最终解决问题的方式与我在上一篇文章中看到的使用鉴别器的方式略有相同。因为我不控制 API 提要,所以我没有鉴别器可以排除,所以我依赖 Json 对象的属性。

需要创建转换器:

public class PolicyConfigurationSettingsConverter : JsonConverter<PolicyConfigurationSettings>
{
    public override PolicyConfigurationSettings Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
    {
        JsonDocument doc;
        JsonDocument.TryParseValue( ref reader, out doc );

        if ( doc.RootElement.TryGetProperty( "minimumApproverCount", out _ ) )
            return JsonSerializer.Deserialize<MinimumNumberOfReviewers>( doc.RootElement.ToString(), options );
        if ( doc.RootElement.TryGetProperty( "useSquashMerge", out _ ) )
            return JsonSerializer.Deserialize<RequireAMergeStrategy>( doc.RootElement.ToString(), options );
        if ( doc.RootElement.TryGetProperty( "scope", out _ ) )
            return JsonSerializer.Deserialize<PolicyConfigurationSettingsScope>( doc.RootElement.ToString(), options );

        return null;
    }

    public override void Write( Utf8JsonWriter writer, [DisallowNull] PolicyConfigurationSettings value, JsonSerializerOptions options )
    {
        if ( value.GetType() == typeof( MinimumNumberOfReviewers ) )
            JsonSerializer.Serialize( writer, ( MinimumNumberOfReviewers )value, options );
        if ( value.GetType() == typeof( RequireAMergeStrategy ) )
            JsonSerializer.Serialize( writer, ( RequireAMergeStrategy )value, options );
        if ( value.GetType() == typeof( PolicyConfigurationSettingsScope ) )
            JsonSerializer.Serialize( writer, ( PolicyConfigurationSettingsScope )value, options );
    }
}

然后需要创建一个JsonSerializerOptions对象来添加Converter

public static JsonSerializerOptions PolicyConfigurationSettingsSerializerOptions()
{
    var serializeOptions = new JsonSerializerOptions();
    serializeOptions.Converters.Add( new PolicyConfigurationSettingsConverter() );
    return serializeOptions;
}

将选项传递到您的 Serializer/Deserializer 语句中。

下面是PolicyConfigurationSettingsclass

public abstract class PolicyConfigurationSettings
{
    [JsonPropertyName( "scope" )]
    public List<PolicyConfigurationScope> Scope { get; set; }
}

public class MinimumNumberOfReviewers : PolicyConfigurationSettings
{
    [JsonPropertyName( "minimumApproverCount" )]
    public int MinimumApproverCount { get; set; }
    [JsonPropertyName( "creatorVoteCounts" )]
    public bool CreatorVoteCounts { get; set; }
    [JsonPropertyName( "allowDownvotes" )]
    public bool AllowDownvotes { get; set; }
    [JsonPropertyName( "resetOnSourcePush" )]
    public bool ResetOnSourcePush { get; set; }
}

public class RequireAMergeStrategy : PolicyConfigurationSettings
{
    [JsonPropertyName( "useSquashMerge" )]
    public bool UseSquashMerge { get; set; }
}

public class PolicyConfigurationSettingsScope : PolicyConfigurationSettings { }

在带有 System.Text.Json.JsonSerializer 的 net 5.0 中,这样的 class 有效:

public class A
{
    public B Data { get; set; }
}
public class B
{
    public long Count { get; set; }
}

正在使用:

System.Text.Json.JsonSerializer.Deserialize<A>("{{\"data\":{\"count\":10}}}", new JsonSerializerOptions { PropertyNameCaseInsensitive = true, IncludeFields = true })

这不是默认设置,这很奇怪。

我用一种更通用的方法解决了这个问题,它介于 NewtonSoft Json 和 .NET Json 的工作方式之间。 使用自定义转换器,我序列化任何多态 class,使用类似于 Newtonsoft 方法的类型标识符,但为了减轻可能的安全风险,您可以选择仅允许内部类型或来自特定程序集的类型。

using System.Text.Json;
using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Collections.ObjectModel;

public class JsonConverterEx<T> : System.Text.Json.Serialization.JsonConverter<T>
{
    private bool _internalOnly = true;
    private string _assembly = String.Empty;

    public JsonConverterEx()
    {
        this._assembly = this.GetType().Assembly.FullName;
    }

    public JsonConverterEx(bool bInternalOnly, string assemblyName)
    {
        _internalOnly = bInternalOnly;
        _assembly = assemblyName;
    }

    public override bool CanConvert(Type typeToConvert)
    {
        Type t = typeof(T);

        if(typeToConvert == t)
            return true;

        return false;
    }

    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        validateToken(reader, JsonTokenType.StartObject);

        reader.Read();      // Move to property name
        validateToken(reader, JsonTokenType.PropertyName);

        var typeKey = reader.GetString();

        reader.Read();      // Move to start of object (stored in this property)
        validateToken(reader, JsonTokenType.StartObject);

        if(!_internalOnly)
        {
            typeKey += ", " + _assembly;
        }

        Type t = Type.GetType(typeKey);
        if(t != null)
        {
            T o = (T)JsonSerializer.Deserialize(ref reader, t, options);
            reader.Read(); // Move past end of item object

            return o;
        }
        else
        {
            throw new JsonException($"Unknown type '{typeKey}'");
        }

        // Helper function for validating where you are in the JSON
        void validateToken(Utf8JsonReader reader, JsonTokenType tokenType)
        {
            if(reader.TokenType != tokenType)
                throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
        }
    }

    public override void Write(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options)
    {
        var itemType = value.GetType();

        writer.WriteStartObject();
        writer.WritePropertyName(itemType.FullName);

        // pass on to default serializer
        JsonSerializer.Serialize(writer, value, itemType, options);

        writer.WriteEndObject();
    }
}

使用方法:

        JsonSerializerOptions op = new JsonSerializerOptions()
        {
            // your usual options here
        };
        op.Converters.Add(new JsonConverterEx<MyExternalClass>(false, "MyAssembly"));
        op.Converters.Add(new JsonConverterEx<MyInternalClass>());

        string s = System.Text.Json.JsonSerializer.Serialize(myobj, op);

        MyInternalClass c = System.Text.Json.JsonSerializer.Deserialize<MyInternalClass>(s, op);