Use JsonConverters to Mimic Dependency Injection During Deserialization

This blog post presents an approach that abstracts access to the entries specified in reference fields in the Contentstack SaaS headless CMS, demonstrating a technique that provides functionality similar to dependency injection during deserialization from JSON with .NET and System.Text.Json.

Contentstack content types can include reference fields, which allow entries to reference other entries. The JSON representation of the referencing entry stores the ID of the referenced entry and its content type identifier. You could use a class such as the following to model these values, and then expose properties of this type in your entry model classes to retrieve values for reference fields.

public class ReferenceProperties
{
    public string Uid { get; set; }

    [JsonPropertyName("_content_type_uid")]
    public string ContentType { get; set; }
}

The code that retrieves the entry JSON that contains the reference could pass the content type and ID to the CMS API and retrieve the referenced entry. Instead of requiring this logic in callers, and for greater convenience, the class that models the reference field could expose that entry to the caller as a property. For this to work, something needs to make this class that knows about the reference values aware of the CMS, which is whatever dependency injection has for the IDeliveryClient interface.

Unfortunately, System.Text.Json, which instantiates these objects during deserialization, does not seem to have great support for dependency injection. Luckily, we can use a JsonConverter to do something similar.

The solution involves a new class named ReferenceField. This class contains a property of the previous ReferenceProperties type that exposes the content type and ID. It also contains a field to store the CMS delivery client that it may use to retrieve the referenced entry. The constructor requires the CMS delivery client as well as the content type and identifier of the referenced entry, which it stores to its members.

For each instance of the ReferenceField class required, deserialization calls the JsonConverter. We get to instantiate the JsonConverter, which means we can pass parameters to it, so we use it to pass the CMS as a parameter to ReferenceFields. The JsonConverter reads the reference identifiers from the JSON into a JsonProperties object, passes that object and the CMS delivery client to the constructor for ReferenceField(), and returns the resulting ReferenceField object, which deserialization sets to a property of the entry model under instantiation.

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

using Deliverystack.DeliveryClient;

public class ReferenceProperties
{
    public string Uid { get; set; }

    [JsonPropertyName("_content_type_uid")]
    public string ContentType { get; set; }
}

public class ReferenceField
{
    public ReferenceProperties EntryData { get; }
    private IDeliveryClient _deliveryClient;

    public ReferenceField(IDeliveryClient deliveryClient, ReferenceProperties stub)
    {
        _deliveryClient = deliveryClient;
        EntryData = stub;
    }

    public object GetEntry()
    {
        return _deliveryClient.AsDefaultEntryModel(
            new EntryIdentifier(EntryData.ContentType, EntryData.Uid));
    }

    public class ReferenceFieldJsonConverter : JsonConverter<ReferenceField>
    {
        private IDeliveryClient _deliveryClient;

        // .NET injection fills the IDeliveryClient dependency 
        public ReferenceFieldJsonConverter(IDeliveryClient deliveryClient)
        {
            _deliveryClient = deliveryClient;
        }

        public override ReferenceField Read(
            ref Utf8JsonReader reader, 
            Type typeToConvert, 
            JsonSerializerOptions options)
        {
            reader.Read();
            ReferenceField field = new ReferenceField(_deliveryClient, (ReferenceProperties)
                JsonSerializer.Deserialize(ref reader, typeof(ReferenceProperties), options));
            reader.Read();
            return field;
        }

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

Remember to add the JsonConverter to serialization options. I am hesitant to implement this in global configuration, so I use a method (conveniently/coincidentally in the CMS HTTP client itself) to retrieve serialization options.

public virtual JsonSerializerOptions GetJsonSerializerOptions()
{
    JsonSerializerOptions options = 
        new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };

    foreach(JsonConverter jsonConverter in GetJsonConverters())
    {
        options.Converters.Add(jsonConverter);
    }

    options.Converters.Add(new ReferenceField.ReferenceFieldJsonConverter(this));
    return options;
}

See also:

One thought on “Use JsonConverters to Mimic Dependency Injection During Deserialization

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: