System.Text.Json and Dynamically Parsing polymorphic objects
.net-core-3.1, c#, system.text.json
Solution
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 { }
Problem
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; } } ```