I am trying to learn how to use System.Text.Json to deserialize JSON to an immutable POCO object that has a copy constructor in addition to a constructor that accepts properties. There is no default constructor.
Reading the dotnet runtime github issue suggests using a JsonConverter to customise deserialization process for immutable types.
I have had a go at this and managed to get it deserializing. However I would like to refactor the deserializer method as it is currently monolothic for deserializing to a 3 level object graph (see below).
At this point I am trying to understand if I can have a JsonConverter for each level in the object graph? For example:
JsonSerializerOptions serializeOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
serializeOptions.Converters.Add(new MotionDetectionConverter(logger),
serializeOptions.Converters.Add(new MotionInfoConverter(logger),
serializeOptions.Converters.Add(new MotionMatricesConverter(matrices)
);
Is it possible to match the Converter to the correct JSON segment or is it easier writing some small helper classes?
internal sealed class MotionDetectionConverter : JsonConverter<MotionDetection>
{
private HashSet<string> _motionDetectionProps;
private HashSet<string> _motionInfoProps;
private HashSet<string> _motionMatrixProps;
private readonly ILogger _log;
public MotionDetectionConverter(ILogger<MotionDetectionConverter> logger)
{
_log = logger;
_motionMatrixProps = new HashSet<string>(new string[] { "x", "y", "width", "height", "tag", "confidence" });
_motionDetectionProps = new HashSet<string>(new string[] { "group", "time", "monitorId", "plug", "details" });
_motionInfoProps = new HashSet<string>(new string[] { "plug", "name", "reason", "matrices", "img", "imgHeight", "imgWidth", "time" });
}
public override MotionDetection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
using (JsonDocument jsonDocument = JsonDocument.ParseValue(ref reader))
{
var jsonObject = jsonDocument.RootElement;
if (!_motionDetectionProps.IsSupersetOf(jsonObject.EnumerateObject().Select(x => x.Name).AsEnumerable()))
{
throw new JsonException();
}
var group = jsonObject.GetProperty("group").ToString();
var time = jsonObject.GetProperty("time").GetDateTimeOffset();
var monitorId = jsonObject.GetProperty("monitorId").ToString();
var plug = jsonObject.GetProperty("plug").ToString();
var details = jsonObject.GetProperty("details");
if (!_motionInfoProps.IsSupersetOf(details.EnumerateObject().Select(x => x.Name).AsEnumerable()))
{
throw new JsonException();
}
var infoPlug = details.GetProperty("plug").ToString();
var infoName = details.GetProperty("name").ToString();
var infoReason = details.GetProperty("reason").ToString();
var infoImg = details.GetProperty("img").ToString();
var infoHeight = details.GetProperty("imgHeight").GetInt32();
var infoWidth = details.GetProperty("imgWidth").GetInt32();
var infoTime = details.GetProperty("time").GetDateTimeOffset();
var infoMatrices = details.GetProperty("matrices").EnumerateArray();
List<MotionLocation> matrices = new List<MotionLocation>();
while (infoMatrices.MoveNext())
{
if (!_motionMatrixProps.IsSupersetOf(infoMatrices.Current.EnumerateObject().Select(prop => prop.Name).AsEnumerable()))
{
throw new JsonException();
}
var x = infoMatrices.Current.GetProperty("x").GetDouble();
var y = infoMatrices.Current.GetProperty("y").GetDouble();
var width = infoMatrices.Current.GetProperty("width").GetDouble();
var height = infoMatrices.Current.GetProperty("height").GetDouble();
var tag = infoMatrices.Current.GetProperty("tag").GetString();
var confidence = infoMatrices.Current.GetProperty("confidence").GetDouble();
matrices.Add(new MotionLocation(x, y, width, height, tag, confidence));
}
MotionInfo info = new MotionInfo(infoPlug, infoName, infoReason, matrices, infoImg, infoHeight, infoWidth, infoTime);
return new MotionDetection(group, time, monitorId, plug, info);
}
}
public override void Write(Utf8JsonWriter writer, MotionDetection motionDetection, JsonSerializerOptions options) =>
writer.WriteStringValue(@"motionDetection");
// to complete....
}
Initially I was using JsonDocument to parse the entire JSON and construct the target object type from the parsed object graph. However, this reads the entire stream to the end, thus making it difficult to nest converters for deserializing subtypes.
So I went down the route of using the Utf8JsonReader to construct the object graph while reading the stream. See below code sample. I am leaving this unanswered for a while to allow other suggestions for deserializing JSON into an immutable type using System.Text.Json, including improvements to the sample code.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using WebApp.Data;
namespace WebApp.Repository.Converters
{
/**
* {
* group: 'myGroup',
* time: 2020-04-24T13:49:16+00:00,
* monitorId: 'myMonitorId',
* plug: TensorFlow-WithFiltering-And-MQTT,
* details: {
* plug: TensorFlow-WithFiltering-And-MQTT,
* name: 'Tensorflow',
* reason: 'object',
* matrices: [{
* "x": 2.313079833984375,
* "y": 1.0182666778564453,
* "width": 373.25050354003906,
* "height": 476.9341278076172,
* "tag": "person",
* "confidence": 0.7375929355621338
* }],
* img: 'base64',
* imgHeight: 64,
* imgWidth: 48,
* time: 2020-04-24T13:49:16+00:00
* }
* }
*/
internal static class MotionDetectionPropertyNames
{
public const string Details = "details";
public const string Group = "group";
public const string MonitorId = "monitorId";
public const string Plug = "plug";
public const string Time = "time";
}
internal struct Detection
{
public MotionInfo details;
public string group;
public string monitorId;
public string plug;
public DateTimeOffset time;
}
internal sealed class MotionDetectionConverter : JsonConverter<MotionDetection>
{
private readonly ILogger _log;
public MotionDetectionConverter(ILogger<MotionDetectionConverter> logger)
{
_log = logger;
}
public override MotionDetection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Detection detection = default(Detection);
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
_log.LogInformation(reader.TokenType.ToString());
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
{
string propertyName = reader.GetString();
switch (propertyName)
{
case MotionDetectionPropertyNames.Details:
{
detection.details = ReadDetails(ref reader);
break;
}
case MotionDetectionPropertyNames.Group:
{
detection.group = MotionConverterHelper.ReadString(ref reader);
break;
}
case MotionDetectionPropertyNames.MonitorId:
{
detection.monitorId = MotionConverterHelper.ReadString(ref reader);
break;
}
case MotionDetectionPropertyNames.Plug:
{
detection.plug = MotionConverterHelper.ReadString(ref reader);
break;
}
case MotionDetectionPropertyNames.Time:
{
detection.time = MotionConverterHelper.ReadIso8601DateTime(ref reader, propertyName);
break;
}
default:
{
MotionConverterHelper.ThrowUnrecognisedProperty(propertyName);
break;
}
}
break;
}
}
}
return new MotionDetection(detection.group, detection.time, detection.monitorId, detection.plug, detection.details);
}
public override void Write(Utf8JsonWriter writer, MotionDetection motionDetection, JsonSerializerOptions options) =>
throw new NotImplementedException("Serialization of MotionDetection objects is not implemented");
private MotionInfo ReadDetails(ref Utf8JsonReader reader)
{
MotionInfo info = default(MotionInfo);
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
{
_log.LogInformation("Reading MotionInfo, handing over to MotionInfo Converter");
using (var logFactory = LoggerFactory.Create(builder => builder.AddConsole()))
{
var logger = logFactory.CreateLogger<MotionInfoConverter>();
JsonSerializerOptions opts = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
opts.Converters.Add(new MotionInfoConverter(logger));
info = JsonSerializer.Deserialize<MotionInfo>(ref reader, opts);
}
break;
}
}
}
if (info is null)
{
throw new JsonException("Failed to details property");
}
return info;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With