Home > OS >  Suggestions for removing repeated code in custom JsonConverters
Suggestions for removing repeated code in custom JsonConverters

Time:09-07

I have quite a large and complex json input schema for one of my services. Because of this the solution involves around 50 custom JsonConverters to handle the different types, polymorphism etc. It all works but there's a considerable amount of repeated code in every converter, the only difference being setting up the local variables, handling the expected properties for that type and constructing it in the EndObject handler:

public class Message
{
    public int Id { get; set; }
    public DateTime SentAt { get; set; }
    public string MessageText { get; set; }

    public Message(int id, DateTime sentAt, string messageText)
    {
        Id = id;
        SentAt = sentAt;
        MessageText = messageText;
    }
}

internal class ExampleConverter : JsonConverter<Message>
{
    public override Message Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("Expected StartObject token");

        var id = Option.None<int>();
        var sentAt = Option.None<DateTime>();
        var messageText = Option.None<string>();

        try 
        {
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.EndObject:
                        return new Message(id, sentAt, messageText);
                    case JsonTokenType.PropertyName:
                        var propName = reader.GetString();
                        reader.Read();

                        switch(propName)
                        {
                            case nameof(Message.Id):
                                id = Option.Some(int.Parse(reader.GetString() ?? string.Empty));
                                break;
                            case nameof(Message.SentAt):
                                sentAt = Option.Some(reader.GetDateTime());
                                break;
                            case nameof(Message.MessageText):
                                messageText = Option.Some(reader.GetString()?.ToUpper());
                                break;
                        }
                        break;
                }
            }
        }
        catch (OptionValueMissingException e)
        {
            throw new Exception(
                $"Missing property of type {e.OptionValueType}", e);
        }
        catch (JsonException e)
        {
            throw new Exception(
                $"Corrupted json", e);
        }
        catch (InvalidOperationException e)
        {
            throw new Exception(
                $"Unexpected type found", e);
        }
        throw new JsonException("Expected EndObject token");
    }

    public override void Write(Utf8JsonWriter writer, Message value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

I was hoping I could create JsonConverterEx<T> derived from JsonConverter<T> and provide my own ReadTypeFromJson method which takes functions to handle the EndObject and PropertyName type, something like this:

public abstract class JsonConverterEx<T> : JsonConverter<T>
{
    protected T ReadTypeFromJson(Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, Func<T> handleJsonEndObject, Action<string> handleJsonPropertyName)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("Expected StartObject token");
        
        try 
        {
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.EndObject:
                        return handleJsonEndObject();
                    case JsonTokenType.PropertyName:
                        var propName = reader.GetString();
                        reader.Read();
                        handleJsonPropertyName(propName);
                        break;
                }
            }
        }
        catch (OptionValueMissingException e)
        {
            throw new Exception(
                $"Missing property of type {e.OptionValueType}", e);
        }
        catch (JsonException e)
        {
            throw new Exception(
                $"Corrupted json", e);
        }
        catch (InvalidOperationException e)
        {
            throw new Exception(
                $"Unexpected type found", e);
        }
        throw new JsonException("Expected EndObject token");
    }
}

and then use it like this:

internal class ExampleConverter : JsonConverterEx<Message>
{
    public override Message Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException("Expected StartObject token");

        var id = Option.None<int>();
        var sentAt = Option.None<DateTime>();
        var messageText = Option.None<string>();

        return ReadTypeFromJson(reader, typeToConvert, options,
            () => new Message(id, sentAt, messageText),
            (propertyName) =>
            {

                switch (propertyName)
                {
                    case nameof(Message.Id):
                        id = Option.Some(int.Parse(reader.GetString() ?? string.Empty));
                        break;
                    case nameof(Message.SentAt):
                        sentAt = Option.Some(reader.GetDateTime());
                        break;
                    case nameof(Message.MessageText):
                        messageText = Option.Some(reader.GetString()?.ToUpper());
                        break;
                }
            });
    }
    
    public override void Write(Utf8JsonWriter writer, Message value, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }
}

Which would then reduce the amount of repeated code in all of my converters but I can't do this because I can't reference the ByRef Utf8JsonReader reader in my handleJsonPropertyName lambda, the calls to reader in the lambda give me Cannot use 'ref' parameter 'reader' inside lambda expression and I can't pass it as a parameter to the lambda because it's ByRef and I get Cannot use 'System.Text.Json.Utf8JsonReader' as a type argument.

I've read up on ByRef structs and understand why I can't do this (so only including above to show 'what I've tried') but is there anything else I can do to avoid all this repeated boilerplate? I need to use Converters as my schema is too complicated for System.Text.Json to be able to work out without.

TIA

Rich

CodePudding user response:

You cannot capture a ref struct in a lambda. And in this case, you shouldn't be anyway, because ReadTypeFromJson has access to the reader. You should instead pass it into the lambda from the ReadTypeFromJson function.

case JsonTokenType.PropertyName:
    var propName = reader.GetString();
    reader.Read();
    handleJsonPropertyName(reader, propName);
    break;

And change the lambda to not capture:

        return ReadTypeFromJson(reader, typeToConvert, options,
            () => new Message(id, sentAt, messageText),
            (reader2, propertyName) =>
            {

                switch (propertyName)
                {
                    case nameof(Message.Id):
                        id = Option.Some<int>(int.Parse(reader2.GetString() ?? string.Empty));
                        break;
                    case nameof(Message.SentAt):
                        sentAt = Option.Some<DateTime>(reader2.GetDateTime());
                        break;
                    case nameof(Message.MessageText):
                        messageText = Option.Some<string>(reader2.GetString()?.ToUpper());
                        break;
                }
            });

However, you also cannot declare the delegate as Action<Utf8JsonReader, string>, so instead you need to make a custom delegate type

public delegate void HandleJsonPropertyNameDelegate(Utf8JsonReader reader, string name);

Then change the declaration for ReadTypeFromJson

protected T ReadTypeFromJson(
    Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options,
    Func<T> handleJsonEndObject, HandleJsonPropertyNameDelegate handleJsonPropertyName)

dotnetfiddle

  • Related