3

I've the following code (.Net Core 3.1):

var dictionary = new Dictionary<string, object>()
{
    {"Key1", 5},
    {"Key2", "aaa"},
    {"Key3", new Dictionary<string, object>(){ {"Key4", 123}}}
};
var serialized = JsonSerializer.Serialize(dictionary, new JsonSerializerOptions()
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
});

I'd expect that serialized variable has a string that looks like:

{"key1":5,"key2":"aaa","key3":{"key4":123}}

However what I get is:

{"key1":5,"key2":"aaa","Key3":{"key4":123}}

I don't really understand why some properties are camelcased and some not. What I was able to figure out is that when a dictionary contains key-value where the value is "primitive type" (int, string, DateTime), it works ok. However, when the value is a complex type (another dicionary, a custom class) it does not work. Any suggestions of how to make the serializer produce keys as camelcase ?

0

1 Answer 1

8

This is a known bug in .NET Core 3.x that has been fixed in .NET 5. See:

If you cannot move to .NET 5 you will need to introduce a custom a custom JsonConverter similar to this one from the Microsoft documentation. The following should do the job:

public class DictionaryWithNamingPolicyConverter : JsonConverterFactory
{
    // Adapted from DictionaryTKeyEnumTValueConverter
    // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-core-3-1#support-dictionary-with-non-string-key
    readonly JsonNamingPolicy namingPolicy;

    public DictionaryWithNamingPolicyConverter(JsonNamingPolicy namingPolicy)
        => this.namingPolicy = namingPolicy ?? throw new ArgumentNullException();
        
    public override bool CanConvert(Type typeToConvert)
    {
        // TODO: Tweak this method to include or exclude any dictionary types that you want.
        // Currently it converts any type implementing IDictionary<string, TValue> that has a public parameterless constructor.
        if (typeToConvert.IsPrimitive || typeToConvert == typeof(string))
            return false;
        var parameters = typeToConvert.GetDictionaryKeyValueType();
        return parameters != null && parameters[0] == typeof(string) && typeToConvert.GetConstructor(Type.EmptyTypes) != null;
    }
    
    public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
    {
        var types = type.GetDictionaryKeyValueType();

        JsonConverter converter = (JsonConverter)Activator.CreateInstance(
            typeof(DictionaryWithNamingPolicyConverterInner<,>).MakeGenericType(new Type[] { type, types[1] }),
            BindingFlags.Instance | BindingFlags.Public,
            binder: null,
            args: new object[] { namingPolicy, options },
            culture: null);

        return converter;
    }       
    
    private class DictionaryWithNamingPolicyConverterInner<TDictionary, TValue> : JsonConverter<TDictionary> 
        where TDictionary : IDictionary<string, TValue>, new()
    {
        readonly JsonNamingPolicy namingPolicy;
        readonly JsonConverter<TValue> _valueConverter;
        readonly Type _valueType;
        
        public DictionaryWithNamingPolicyConverterInner(JsonNamingPolicy namingPolicy, JsonSerializerOptions options)
        {
            this.namingPolicy = namingPolicy ?? throw new ArgumentNullException();
            // For performance, cache the value converter and type.
            this._valueType = typeof(TValue);
            this._valueConverter = (_valueType == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter 
        }

        public override TDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (reader.TokenType == JsonTokenType.Null)
                return default(TDictionary); // Or throw an exception if you prefer.
            if (reader.TokenType != JsonTokenType.StartObject)
                throw new JsonException();
            var dictionary = new TDictionary();
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                    return dictionary;
                if (reader.TokenType != JsonTokenType.PropertyName)
                    throw new JsonException();
                var key = reader.GetString();
                reader.Read();                  
                dictionary.Add(key, _valueConverter.ReadOrDeserialize<TValue>(ref reader, _valueType, options));
            }
            throw new JsonException();
        }
        
        public override void Write(Utf8JsonWriter writer, TDictionary dictionary, JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            foreach (var pair in dictionary)
            {
                writer.WritePropertyName(namingPolicy.ConvertName(pair.Key));
                _valueConverter.WriteOrSerialize(writer, pair.Value, _valueType, options);
            }
            writer.WriteEndObject();
        }
    }
}

public static class JsonSerializerExtensions
{
    public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, Type type, JsonSerializerOptions options)
    {
        if (converter != null)
            converter.Write(writer, value, options);
        else
            JsonSerializer.Serialize(writer, value, type, options);
    }

    public static T ReadOrDeserialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => converter != null ? converter.Read(ref reader, typeToConvert, options) : (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);
}

public static class TypeExtensions
{
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
        => (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();

    public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
        => type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());

    public static Type [] GetDictionaryKeyValueType(this Type type)
        => type.GetDictionaryKeyValueTypes().SingleOrDefaultIfMultiple();
}

public static class LinqExtensions
{
    // Copied from this answer https://stackoverflow.com/a/25319572
    // By https://stackoverflow.com/users/3542863/sean-rose
    // To https://stackoverflow.com/questions/3185067/singleordefault-throws-an-exception-on-more-than-one-element
    public static TSource SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
    {
        var elements = source.Take(2).ToArray();
        return (elements.Length == 1) ? elements[0] : default(TSource);
    }
}

Then serialize as follows:

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
    Converters = { new DictionaryWithNamingPolicyConverter(JsonNamingPolicy.CamelCase) },
};
var serialized = JsonSerializer.Serialize(dictionary, options);

Demo fiddle here.

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

3 Comments

Great, thanks a lot. I cannot use .Net 5 so I will use the solution with a custom JsonConverter.
I am using .net6 or even 7 and I still get this error when deserializing dictionaries in the swagger UI json.
@Shilan and for those of you looking here for why deserializing a dictionary object won't adhere to the DictionaryKey Policy. Here is from the MS docs: "The camel case naming policy for dictionary keys applies to serialization only. If you deserialize a dictionary, the keys will match the JSON file even if you specify JsonNamingPolicy.CamelCase for the DictionaryKeyPolicy." learn.microsoft.com/en-us/dotnet/standard/serialization/…

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.