1

I'm currently using the following code to set up a System.Text.Json JsonSerializerOptions object that alphabetizes properties upon serialization:

public static readonly JsonSerializerOptions serializerOptions = new()
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers =
        {
            AlphabetizeProperties,
        }
    }
}

private static void AlphabetizeProperties(JsonTypeInfo info)
{
    if (info.Kind != JsonTypeInfoKind.Object) return;

    List<JsonPropertyInfo> sortedProperties = [];
    sortedProperties.AddRange(info.Properties);
    sortedProperties.Sort((p1, p2) => string.CompareOrdinal(p1.Name, p2.Name));

    info.Properties.Clear();

    for (int i = 0; i < sortedProperties.Count; i++)
    {
        sortedProperties[i].Order = i;
        info.Properties.Add(sortedProperties[i]);
    }
}

I'm in the process of trying to convert our code to use System.Text.Json's source generation feature so that we can turn on trimming and eventually move to NativeAOT. However, I cannot find any way to insert modifiers into the JsonSerializerContext.

Things I've tried:

  • The JsonSourceGenerationOptions attribute has no way to insert modifiers.
  • I tried using WithAddedModifier on my JsonSerializerContext. I can, in a static constructor, initialize the JsonSerializerOptions, manually construct a new instance of my JsonSerializerContext, and overwrite the Default property. This is similar to the approach used here, for a different purpose. However, the dependency is circular; I need the JsonSerializerOptions to initialize the JsonSerializerContext so I can call WithAddedModifiers on it, and I need to pass the JsonSerializerContext to JsonSerializerOptions.TypeInfoResolver to initialize it.
  • I tried obtaining the JsonTypeInfo instance via JsonSerializerContext.Default.MyObject and calling AlphabetizeProperties directly on it. This throws an InvalidOperationException with the following message: This JsonTypeInfo instance is marked read-only or has already been used in serialization or deserialization.

At this point, it seems my only option is to copy the source generator wholesale and re-order the properties within my new generator. Short of taking this drastic step, is there any way to achieve what I'm looking for?

1 Answer 1

1

You can use modifiers with source generation, as long as you do the following:

  1. You cannot alphabetize the properties of a JsonSerializerContext because, as you have noted, it is immutable once constructed, you will get an InvalidOperationException if you try. But you can still use JsonTypeInfoResolver.WithAddedModifier() because this extension method returns a new IJsonTypeInfoResolver that encapsulates the existing resolver.

    The inconvenience here is that you lose the ability to use the predefined JsonTypeInfo<T> properties on your typed serialization context.

  2. There does look to be a chicken-and-egg problem that constructing your options requires your resolver which requires calling WithAddedModifier() which requires the options you are constructing. However, this chicken-and-egg problem exists only when working exclusively with typed serialization contexts. Since you need to abandon them anyway and use the more general IJsonTypeInfoResolver, you can simply construct options containing the modified serialization context, and work with those options.

    You will get enormous numbers of IL2026 and IL3050 warnings once you do, but you can at least minimize them by backporting (from .NET 11) extension methods that get a JsonTypeInfo<T> from some JsonSerializerOptions given an instance of type T:

    [API Proposal]: JsonSerializerOptions.GetTypeInfo<T> #118468.

Thus your modifiers and extension methods should look something like:

public static class JsonExtensions
{
    public static void AlphabetizeProperties(JsonTypeInfo typeInfo)
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object) 
            return;
        foreach (var (property, index) in typeInfo.Properties.OrderBy(p => p.Name, StringComparer.Ordinal).Select((p, i) => (p, i)))
            property.Order = index;
    }

    // Two API methods approved for .NET 11: https://github.com/dotnet/runtime/issues/118468
    // These will throw if the JsonTypeInfo<T> is not found.
    public static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options, T value) => 
        (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
    public static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options) => 
        (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
}

Then, construct and save your JsonSerializerOptions e.g. as follows:

public static JsonSerializerOptions AlphabetizedOptions { get; } = new ()
{
    TypeInfoResolver = MySerializationContext.Default
        .WithAddedModifier(JsonExtensions.AlphabetizeProperties),
    // Add other options as required, e.g.:
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

Then, when serializing some Model model object, use the options as follows:

var typeInfo = AlphabetizedOptions.GetTypeInfo(model);
var json = JsonSerializer.Serialize(model, typeInfo);
var model2 = JsonSerializer.Deserialize(json, typeInfo);

Notes:

  • I tested using .NET 10.0.0-preview.5 with the following settings in my csproj:

    <PublishAot>true</PublishAot>
    <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
    

    Setting JsonSerializerIsReflectionEnabledByDefault to false ensures that System.Text.Json only uses the source-generated contexts provided to it and never falls back on reflection-based serialization. Thus it is appropriate to use when testing your usage of source generation, especially when you are working with resolver instances declared only as IJsonTypeInfoResolver.

  • Setting JsonPropertyInfo.Order is sufficient to order the properties.

  • I did not test with serialization-optimized fast path mode.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for the very detailed answer. I gave it a try and it seems to work great. It is too bad that I now get runtime exceptions rather than compile warnings for non-included types, but that's an acceptable price to pay. I wonder if there is some DynamicallyAccessedMemberTypes attribute I might be able to throw on the GetTypeInfo method that would help with that.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.