12

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.

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
edhenn
  • 303
  • 3
  • 7
  • 2
    They aren't meant to, nor do they have to. The JsonConverter options apply to ASP.NET request and response parsing. The Microsoft.Extensions.Configuration is a separate package used by *all* types of projects, including console projects. `appsettings.json` isn't a special file, it's just the conventional name used to load settings using one of the many available configuration providers. The JSON provider loads setting using a specific format, described in the docs. *All* settings, no matter the source (json, ini, database, environment), are stored as key/value pairs. – Panagiotis Kanavos Jul 09 '20 at 17:23
  • Why do you want to use a JsonConverter? You can't use it to construct objects, those are constructed from the key/value pairs loaded from all providers. If you want to convert the loaded values to uppercase - why? If case matters, shouldn't you be preserving case, or making sure the *correct* case was stored? If it doesn't you can use case-insensitive comparisons or case-insensitive dictionaries for those values – Panagiotis Kanavos Jul 09 '20 at 17:27
  • If you want to load settings in a specific way you can create your own settings provider. I suspect the real problem is the need for uppercase though – Panagiotis Kanavos Jul 09 '20 at 17:27
  • If you require upper-case settings, you need to apply the conversion to *all* settings, no matter the source. This means you should convert the values during binding, not during loading from one specific provider. Better yet, why not change `MyString`'s setter to store the value as uppercase? – Panagiotis Kanavos Jul 09 '20 at 17:31
  • No I don't require uppercase, it was just a simple thing to do in a converter for this question. Your first comment about the intended use of extensions is helpful, as is the comment about creating my own settings provider. – edhenn Jul 09 '20 at 17:33
  • *Why*? You probably don't need one, unless you have an external file that doesn't follow the `section:subsection:key:value` form. Every custom provider should convert settings to that form though. – Panagiotis Kanavos Jul 09 '20 at 17:34
  • 1
    I guess the specific answer I was looking for here is that JsonConfigurationProvider never calls ```JsonSerializer.Deserialize```, because as you point out it doesn't need to, it's just for creating key-value pairs. It calls ```JsonDocument.Parse``` for that work instead. Thanks for your help. – edhenn Jul 10 '20 at 21:00

1 Answers1

9

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.

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138