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