Douglas Reid
Douglas Reid

Reputation: 3788

How to globally set default options for System.Text.Json.JsonSerializer?

Instead of this:

JsonSerializerOptions options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    // etc.
};
var so = JsonSerializer.Deserialize<SomeObject>(someJsonString, options);

I would like to do something like this:

// This property is a pleasant fiction
JsonSerializer.DefaultSettings = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    // etc.
};

// This uses my options
var soA = JsonSerializer.Deserialize<SomeObject>(someJsonString); 

// And somewhere else in the same codebase...
// This also uses my options
var soB = JsonSerializer.Deserialize<SomeOtherObject>(someOtherJsonString); 

The hope is to not have to pass an instance of JsonSerializerOptions for our most common cases, and override for the exception, not the rule.

As indicated in this q & a, this is a useful feature of Json.Net. I looked in the documentation for System.Text.Json as well as this GitHub repo for .NET Core. And this one.

There doesn't seem to be an analog for managing JSON serialization defaults in .NET Core 3. Or am I overlooking it?


Upvotes: 103

Views: 94521

Answers (15)

capslo
capslo

Reputation: 45

Based on @Elijah's suggestion about registering IOptions, here's a complete solution that shows how to properly configure and use JSON serialization options globally.

Solution

First, configure the options in Program.cs:

That's for MVC

 services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
        options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
        options.JsonSerializerOptions.WriteIndented = false;
        options.JsonSerializerOptions.MaxDepth = 18;
        options.JsonSerializerOptions.AllowTrailingCommas = true;
        options.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
    });

That's for miniamApi

    services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
    {
       options.SerializerOptions.PropertyNameCaseInsensitive = true;
       options.SerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString;
       options.SerializerOptions.WriteIndented = false;
       options.SerializerOptions.MaxDepth = 18;
       options.SerializerOptions.AllowTrailingCommas = true;
       options.SerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
    });

Then inject and use these options in your services.If you need you can ovveride in constructor.:

public class TestService : ITestService
{
   private readonly JsonSerializerOptions _jsonSerializerOptions;

   public TestService(IOptions<JsonOptions> jsonOptions)
   {
       _jsonSerializerOptions = jsonOptions.Value.SerializerOptions;
   }

   public async Task<ModelDto> DeserializeObjectAsync(MemoryStream ms)
   {
       return await JsonSerializer.DeserializeAsync<ModelDto>(
           ms, 
           _jsonSerializerOptions
       );
   }
}

Upvotes: 0

user2022862
user2022862

Reputation: 21

I had to use a different converter for enums in my http client. In my case to avoid using the options in each json conversion, and due to not having the option to change the default options (unless using reflection from above). I created a converter and used it as attribute above the enum

public class EnumStringConverter<TEnum> : JsonConverter<TEnum> where TEnum : Enum
{
    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        string enumString = reader.GetString()!;
        return (TEnum)Enum.Parse(typeof(TEnum), enumString, true);
    }

    public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value.ToString());
    }
}

Enum:

[JsonConverter(typeof(EnumStringConverter<MyCoolEnum>))]
public enum MyCoolEnum {A,B,C}

Upvotes: 2

Rick Penabella
Rick Penabella

Reputation: 459

Using .NET 8 prerelease and it even includes snake case!

.ConfigureHttpJsonOptions(options => {
          options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
})

Upvotes: 8

user2738103
user2738103

Reputation: 29

For ASP.NET Core 7.0+:

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.PropertyNameCaseInsensitive = true;
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.AllowTrailingCommas = true;
    options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    options.SerializerOptions.ReferenceHandler =ReferenceHandler.IgnoreCycles;
});

The method HttpJsonServiceExtensions.ConfigureHttpJsonOptions():

Configures options used for reading and writing JSON when using Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync and Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync. JsonOptions uses default values from JsonSerializerDefaults.Web.

This method is available starting from ASP.NET Core 7.0.

Upvotes: 2

Kato
Kato

Reputation: 73

As @elijah commented, injecting IOptions<JsonOptions> jsonOptions worked for me. I was then able to use the JsonSerializerOptions property. I noticed no differences between this and what I had configured using AddJsonOptions() in my Program.cs file.

Upvotes: 2

Ricardo Boss
Ricardo Boss

Reputation: 151

I looked for a solution to use my source generated context as the default serializer, but the default options are still read-only.

I'll just leave this here in case it helps someone else: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation?pivots=dotnet-8-0#source-generation-support-in-aspnet-core

The solution there looks like this:

[JsonSerializable(typeof(WeatherForecast[]))]
internal partial class MyJsonContext : JsonSerializerContext { }
var serializerOptions = new JsonSerializerOptions
{
    TypeInfoResolver = MyJsonContext.Default;
};

services.AddControllers().AddJsonOptions(
    static options =>
        options.JsonSerializerOptions.TypeInfoResolverChain.Add(MyJsonContext.Default));

Upvotes: 5

ps2goat
ps2goat

Reputation: 8475

You can create an extension method. Here's an example (UPDATE 2023-10-27: Pulled the code to the bottom of this answer for completeness)

I use separate methods vs having to build special settings, so that all the settings will be in a single spot and easily reusable.

public static class DeserializeExtensions
{
    private static JsonSerializerOptions defaultSerializerSettings = new JsonSerializerOptions();
    
    // set this up how you need to!
    private static JsonSerializerOptions featureXSerializerSettings = new JsonSerializerOptions();


    public static T Deserialize<T>(this string json)
    {       
        return JsonSerializer.Deserialize<T>(json, defaultSerializerSettings);
    }
    
    public static T DeserializeCustom<T>(this string json, JsonSerializerOptions settings)
    {
        return JsonSerializer.Deserialize<T>(json, settings);
    }
    
    public static T DeserializeFeatureX<T>(this string json)
    {
        return JsonSerializer.Deserialize<T>(json, featureXSerializerSettings);
    }
}

Then you call it as a method on a string, whether literal or a variable.

    Car result = @"{""Wheels"": 4, ""Doors"": 2}".DeserializeFeatureX<Car>();

Updated 2023-10-27 I revisited this answer and realized that the example code was never included here. I have copied it here in case something happens to the linked code. It was originally targeting .NET Core 3.x but last confirmed with .NET 7.

using System;
using System.Text.Json;
                    
public class Program
{
    public static void Main()
    {
        Console.WriteLine("Hello World");
        
        
        // "json".Deserialize<object>();
        Car result = @"{""Wheels"": 4, ""Doors"": 2}".DeserializeFeatureX<Car>();
        
        Console.WriteLine($"Doors: { result.Doors }, Wheels: { result.Wheels }");
    }
}

public static class DeserializeExtensions
{
    private static JsonSerializerOptions defaultSerializerSettings =
        new JsonSerializerOptions();
    
    // set this up how you need to!
    private static JsonSerializerOptions featureXSerializerSettings =
        new JsonSerializerOptions();


    public static T Deserialize<T>(this string json)
    {       
        return JsonSerializer.Deserialize<T>(json, defaultSerializerSettings);
    }
    
    public static T DeserializeCustom<T>(this string json, JsonSerializerOptions settings)
    {
        return JsonSerializer.Deserialize<T>(json, settings);
    }
    
    public static T DeserializeFeatureX<T>(this string json)
    {
        return JsonSerializer.Deserialize<T>(json, featureXSerializerSettings);
    }
}

public class Car
{
  public int Wheels { get; set; }
  public int Doors { get; set; }
}

Upvotes: 43

J Scott
J Scott

Reputation: 1019

Found this looking for some inspiration. We have a web API that calls some other APIs. The third-party APIs may use camel casing names, or they may use kebab. Generally, each is internally consistent with itself, but the naming convention changes between APIs. I needed to configure the options in a limited scope but not contaminate other projects.

I ended up making an options object, called such not to confuse with settings, specific to each project (set visibility to internal), surfaced through DI, that had the JSON settings as a property. Project-specific settings, like how an arbitrary API names properties, are contained to that project, and each project is responsible for setting its defaults.

I've found it's better, especially for library code, to be specific and deal with passing around the options object than to try and set up full app defaults only to limit interoperability. This also lets me avoid marking property names on my DTOs, so I can serialize the DTO outbound from my top-level API and not have explicit JSON property names breaking the API-level conventions.

If you do have to support an atypical property-specific name, you can use an internal property with getters and setters to your public undecorated property to isolate the change.

Upvotes: -1

PWC
PWC

Reputation: 31

In case one also needs to add custom converters to the default JsonSerializerOptions (System.Text.Json >= 7.0.0) - without the use of controllers - one can use the following trick:

var jsonConverterList = new List<JsonConverter>
{
    new YourCustomConverter1(),
    new YourCustomConverter2()
};

Type type = AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(assembly => assembly.GetTypes())
    .SingleOrDefault(t => t.FullName == "System.Text.Json.JsonSerializerOptions+ConverterList");
object[] paramValues = new object[] { JsonSerializerOptions.Default, jsonConverterList };
var converterList = type!.GetConstructors()[0].Invoke(paramValues) as IList<JsonConverter>;
typeof(JsonSerializerOptions).GetRuntimeFields().Single(f => f.Name == "_converters")
    .SetValue(JsonSerializerOptions.Default, converterList);

The property JsonSerializerOptions.Default.Converters does not allow adding items, as is is immutable, therefore this trick replaces the converterList alltogether. And since ConverterList is a private sealed class invoking its constructor requires reflection.

Upvotes: 3

SerjG
SerjG

Reputation: 3570

Some fields has became auto-properties starting .NET7. So there is no way to change it as in earlier answers.

New approach is to change the private fields directly:

public static void SetIgnoreNulls() => typeof(JsonSerializerOptions).GetRuntimeFields()
        .Single(f => f.Name == "_defaultIgnoreCondition")
        .SetValue(JsonSerializerOptions.Default, JsonIgnoreCondition.WhenWritingNull);

Why GetRuntimeFields().Single() and not just GetRuntimeField(%name%)? Answer is here: https://github.com/dotnet/runtime/issues/15643

Upvotes: 4

Douglas Reid
Douglas Reid

Reputation: 3788

A workaround has been proposed by GitHub user andre-ss6 as follows:

((JsonSerializerOptions)typeof(JsonSerializerOptions)
    .GetField("s_defaultOptions", 
        System.Reflection.BindingFlags.Static |
        System.Reflection.BindingFlags.NonPublic).GetValue(null))
    .PropertyNameCaseInsensitive = true;

Update [2023-02-17]: but for NET7 see this answer.

Upvotes: 12

John
John

Reputation: 501

This seemed to work for me, in StartUp.ConfigureServices:

services.AddControllers().AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        options.JsonSerializerOptions.PropertyNamingPolicy=JsonNamingPolicy.CamelCase;
    });

Upvotes: 33

Douglas Reid
Douglas Reid

Reputation: 3788

The default options are not exposed in JsonSerializer for .NET Core 3.1. However, as of December, 2019 this has been added to the road map for 5.0.

The release of .NET 5.0 is expected November, 2020. But there's no guarantee this particular issue will be addressed at any particular time. Other than waiting, these answers suggest workarounds:

Also, I packaged my convenience extension methods, inspired by @ps2goat's answer and put them on nuget.org and github:

Upvotes: 13

tmaj
tmaj

Reputation: 34947

(If you ever switch to using Json.NET)

I prefer and recommend being explicit and pass settings to all calls, but you can set defaults with DefaultSettings.

JsonConvert.DefaultSettings = () => MySuperJsonSerializerSettings;

and then

var json = JsonConvert.SerializeObject(o1);
var o2 = JsonConvert.DeserializeObject(x);

Upvotes: -4

Chris Yungmann
Chris Yungmann

Reputation: 1259

No, JsonSerializerOptions does not expose the default options. If you are using a particular web framework there may be a way to specify (de-)serialization settings through that. Otherwise, I suggest creating your own convenience methods.

See also this open issue.

Upvotes: 23

Related Questions