Search code examples
c#jsonxmldeserialization

How to deserialize mixed JSON and XML to classes


I'm getting a response string from Azure DevOps REST API that contain mostly JSON, with some XML sprinkled into a few property values. I build a number of C# classes replicating the JSON structure of the response, in order to deserialize the response.

When

var response = client.Get(request);
if (response.StatusCode == HttpStatusCode.OK
    && response.Content != null)
{
    return JsonSerializer.Deserialize<T>(response.Content);
}

reaches the XML in

public class Fields
{
    ...
    [JsonPropertyName("Microsoft.VSTS.TCM.Steps")]
    public Steps MicrosoftVSTSTCMSteps { get; set; }
}

I get a

System.Text.Json.JsonException: 'The JSON value could not be converted to ADO.Response.Steps. Path: $.fields['Microsoft.VSTS.TCM.Steps'] | LineNumber: 0 | BytePositionInLine: 5149.'

The Steps class is defined like

[XmlRoot(ElementName = "steps")]
public class Steps
{
    [XmlElement(ElementName = "step")]
    public Step[] Step { get; set; }

    [XmlAttribute(AttributeName = "id")]
    public string Id { get; set; }

    [XmlAttribute(AttributeName = "last")]
    public string Last { get; set; }

    ...
}

An exerpt of the incomming JSON/XML string looks like this:

{
  ...
  "Microsoft.VSTS.TCM.Steps":"<?xml version="1.0"?><steps last="5" id="0"><step id="2"  type="ActionStep"><parameterizedString isformatted="true"></parameterizedString><description /></step></steps><xml/>
  ...
}

How do I deserialize the JSON and the XML without detaching the Steps class from the hierarchy by e.g keeping the XML as a string in the deserialized JSON?

I'm looking to avoid post processing like this

XmlSerializer XmlSerializer = new XmlSerializer(typeof(Steps));
Steps steps = (Steps)XmlSerializer.Deserialize(new StringReader(resp.fields.MicrosoftVSTSTCMSteps));


Solution

  • A custom converter became my best solution, using System.Text.Json, System.Text.Json.Serialization and System.XML.Serialization. I can now use the regular property attributes and keep the deserialization in one neat flow.

            // Request an ADO resource and deserialize the JSON response with embedded XML into C# classes.
            private static T? Get<T>(string resource, RestClient client)
            {
                var request = new RestRequest()
                {
                    Resource = resource
                };
    
                var response = client.Get(request);
                if (response.StatusCode == HttpStatusCode.OK
                    && response.Content != null)
                {
                    // Create a custom deserializer for mixed json and xml content
                    var options = new JsonSerializerOptions()
                    {
                        Converters = { new MixedJsonXmlConverter() }
                    };
    
                    // Deserialize mixed json and xml content from response
                    return JsonSerializer.Deserialize<T>(response.Content, options);
                }
    
                return default;
            }
    

    The custom converter that picks up on Steps properties that contain XML targeted for deserialization, looks like this:

        internal class MixedJsonXmlConverter : JsonConverter<Steps>
        {
            // Deserialize Microsoft.VSTSTCMSteps of Steps data type stored in the JSON response, as XML.
            // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/converters-how-to?pivots=dotnet-6-0#registration-sample---converters-collection
            // Support property attributes equal to stock deserialization of JSON and XML.
            public override Steps? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
            {
                // Read XML string and parse it to object of type Steps
                var xml = reader.GetString();
                var serializer = new XmlSerializer(typeof(Steps));
    
                using var stringReader = new StringReader(xml);
                return (Steps)serializer.Deserialize(stringReader);
    
            }
    
            public override void Write(Utf8JsonWriter writer, Steps value, JsonSerializerOptions options)
            {
                throw new NotImplementedException();
            }
        }
    

    If I bump into other JSON properties that needs to be deserialized as XML, I can use the same pattern.

    So now I can address the response as one cohesive data structure like:

    var workItem = Get<CaseWorkItem>(resource, client);
    
    Console.WriteLine($"Property in JSON response: {workItem.fields.SystemAreaPath}");
    Console.WriteLine($"Property in XML response: {workItem.fields.MicrosoftVSTSTCMSteps.Id}")