Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

System.Text.Json and Dynamically Parsing polymorphic objects

I don't believe I am wrapping my head around how to properly use JsonConverter for polymorphism in parsing json results.

In my scenario, I am targeting Git Policy Configurations in TFS. A policy configuration:


"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"
        },
{...}]

More settings examples: Require a Merge Strategy

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

Required Reviewers

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

In the json snippet above, the settings object is different based on the type of configuration.

What is the best approach to writing a converter than can dynamically serialize/deserialize the settings object? I've read a couple of articles regarding this and can't quite wrap my head around it.


This is how I am currently deserializing all of my API results, so far they have been simple result sets.

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;
}

My integration test.

PolicyConfiguration is the type I am trying to deserialize to.

[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 );
}

My current classes for this parsing situation

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; }
}
like image 667
MattB Avatar asked Mar 21 '20 19:03

MattB


People also ask

Is polymorphic deserialization possible in System text JSON?

Text. Json doesn't support the serialization of polymorphic type hierarchies. For example, if a property's type is an interface or an abstract class, only the properties defined on the interface or abstract class are serialized, even if the runtime type has additional properties.

What is polymorphic JSON?

You can use this schema when defining XML Type hierarchies by using only the base XML Types. The XML schema defines XML Types that inherit from each other. In the JSON, an object carries no additional information about the type.

What is System Text JSON?

Text. Json. Serialization namespace, which contains attributes and APIs for advanced scenarios and customization specific to serialization and deserialization.

What does Jsonconvert Deserializeobject do?

Deserializes the JSON to the specified . NET type. Deserializes the JSON to the specified . NET type using a collection of JsonConverter.


2 Answers

I ended up solving my issue in slightly the same way I had seen a previous article using a discriminator. Since I do not control the API feeds, I do not have a discriminator to drive off of, so I am relying on the properties of the Json object.

Need to create a Converter:

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 );
    }
}

Then need to create a JsonSerializerOptions object to add the Converter

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

Pass the options into your Serializer/Deserializer statement.

Below is the PolicyConfigurationSettings class

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 { }
like image 186
MattB Avatar answered Oct 20 '22 00:10

MattB


In net 5.0 with System.Text.Json.JsonSerializer, what works for a class like this:

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

is using:

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

which is weird that is not the default.

like image 37
lnaie Avatar answered Oct 19 '22 22:10

lnaie