Search code examples
c#asp.net-core.net-coreapplication-settingsjsonconverter

Asp.Net Core 3.1 Appsettings not respecting JsonConverter


In asp.net core 3.1, using the new System.Text.Json, I am trying to use a custom JsonConverter on an appsettings section. Manually serializing/deserializing respects the converter just fine, but reading from appSettings via Options pattern does not. Here's what I have:

The JsonConverter. For simplicity, this one just converts a string value to uppercase:

    public class UpperConverter : JsonConverter<string>
    {
        public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
            reader.GetString().ToUpper();

        public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
            writer.WriteStringValue(value == null ? "" : value.ToUpper());
    }

The appsettings class, declaring the converter on a string property:

    public class MyOptions
    {
        public const string Name = "MyOptions";
        [JsonConverter(typeof(UpperConverter))]
        public string MyString { get; set; }
    }

The Startup.cs changes to prepare everything:

       public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews()
                .AddJsonOptions(options =>
                {
                    options.JsonSerializerOptions.Converters.Add(new UpperConverter());
                });

            services.Configure<MyOptions>(Configuration.GetSection(MyOptions.Name));
        }

When I inject an IOptions<MyOptions> into the HomeController, it reads a lowercase value. If I manually do JsonSerializer.Deserialize<MyOptions>("{\"MyString\":\"lowercase_manual\"}"), I get an uppercase string. Even when I remove Startup declarations of JsonSerializerOptions.

Does anyone know how to get the appsettings / options pattern to respect the JsonConverter? Do I have to declare the JsonSerializerOptions somewhere else? Thanks.


Solution

  • It's important to understand that the options pattern is implemented as two separate steps: reading data from a configuration source, then binding that data to strongly-typed objects.

    The read step is implemented by various configuration providers, only one of which is JSON. You might expect that the JSON provider would respect your JsonConverter, but this step only performs minimal transformation of its configuration data into a generic format that the next step can accept.

    The binding step, then, would seem to be the place that would care about JsonConverter. But this step is intentionally completely agnostic of any specific configuration provider, because it simply receives data in a generic format from the providers of which it purposefully knows nothing about. Therefore, it won't care about a JSON-specific converter.

    It will, however, care about more generic converters to handle its generic data, and fortunately .NET already has infrastructure for this built in: type converters. These have been in .NET since almost the beginning, and while they're old they're perfectly simple, serviceable, and indeed ideal for this specific scenario.

    A full example of how to implement a type converter is out of scope for this answer, but the essentials are that you derive from TypeConverter, override the appropriate methods, and decorate the class you want to be converted with a TypeConverterAttribute pointing back to your TypeConverter implementation. Then it should all Just Work™.


    The caveat with the example you've provided is that you aren't actually trying to convert anything, you're trying to transform a string, and obviously a TypeConverter won't be invoked since the source value from the configuration providers is a string, while the destination type on your options class is also a string.

    What you can do instead is create a new class that wraps a string to force it to uppercase:

    public class UppercaseString
    {
        public string Value { get; }
    
        public UppercaseString(string str)
        {
            Value = str.ToUpper();
        }
    
        public static implicit operator string(UppercaseString upper)
            => upper.Value;
    }
    

    then change your options class to use that wrapper:

    public class MyOptions
    {
        public const string Name = "MyOptions";
    
        public UppercaseString MyString { get; set; }
    }
    

    and finally, implement a TypeConverter that converts from string to UppercaseString.

    Note the definition of implicit operator string - this allows you to use an UppercaseString anywhere a standard string is expected, so that you don't have to change your code that references MyOptions.MyString to MyOptions.MyString.Value.