0

I need to use JsonNamingPolicy.CamelCase to serialize my object.

But when I use Dictionary with [JsonExtensionData] attribute, it seems something not work correctly.

Example Code.

public class Test
{
    public Dictionary<string, object> Labels { get; set; } = new();

    [JsonExtensionData]
    public Dictionary<string, object> MetaInfo { get; set; } = new();

    public Dictionary<string, object>? Nullable { get; set; } = null;
}

public class TestContent
{
    public string? Value { get; set; }
}

System.Text.Json

var options = new JsonSerializerOptions
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
};

var test = new Test
{
    Labels = new Dictionary<string, object>
    {
        { "Test1", new TestContent{ Value = "TestContent1" } }, 
        { "Test2", new TestContent { Value = null } },
    },
    MetaInfo = new Dictionary<string, object>
    {
        { "Test1", new TestContent{ Value = "TestContent1" } },
        { "Test2", new TestContent { Value = null } },
    }
};

var result = JsonSerializer.Serialize(test, options);
System.Console.WriteLine(result);

Expected output

  • Observe that the properties are named "test1" and "test2".
{
    "labels":{
        "test1":{
            "value": "TestContent1"
        },
        "test2":{
        }
    },
    "test1":{
        "value": "TestContent1"
    },
    "test2":{
    }
}

Actual output

  • Observe that the properties are named "Test1" and "Test2" instead.
{
    "labels":{
        "test1":{
            "value": "TestContent1"
        },
        "test2":{
            
        }
    },
    "Test1":{
        "value": "TestContent1"
    },
    "Test2":{
        
    }
}

You can see the JsonNamingPolicy.CamelCase work correct on labels, but work failed on Test1 and Test2. Am I missing some settings?

When I change to Newtonsoft.Json it works fine:

var contractResolver = new DefaultContractResolver
{
    NamingStrategy = new CamelCaseNamingStrategy
    {
        ProcessDictionaryKeys = true,
        ProcessExtensionDataNames = true,
        OverrideSpecifiedNames = false,
    }
};

var settings = new JsonSerializerSettings
{
    ContractResolver = contractResolver,
    NullValueHandling = NullValueHandling.Ignore,
};

var result = JsonConvert.SerializeObject(test, settings);       
System.Console.WriteLine(result);
// {"labels":{"test1":{"value":"TestContent1"},"test2":{}},"test1":{"value":"TestContent1"},"test2":{}}
Changemyminds
  • 1,147
  • 9
  • 25
  • BTW, when using `System.Text.Json`'s `[JsonExtensionData]` use a `Dictionary` instead of `Dictionary`. – Dai Sep 08 '22 at 14:41
  • I think it's by-design: the whole point of `JsonExtensionData` is to _capture and **preserve**_ any JSON object properties not explicitly declared/defined in the `class` representing the JSON object's type - so it makes sense to preserve those property names _verbatim_ so that you can re-save the JSON without impacting any other consumers that might expect `TitleCase` (not `camelCase`) property names. – Dai Sep 08 '22 at 14:47
  • Thank you @Dai for your reply. So if I want my result to be the same as `Newtonsoft.Json`, what should I do? Can `System.Text.Json` do it? – Changemyminds Sep 08 '22 at 15:03
  • No, System.Text.Json cannot do it unless you write your own custom JsonConverter that inspects properties for the JsonExtensionData attribute and builds the Json output manually so as to avoid the JsonSerializer ever seeing the attribute. (I don't know, but to me that doesn't seem to be worth the effort...) –  Sep 08 '22 at 15:05
  • 1
    FYI: Here some (short) discussion in dotnet/runtime github's issue tracker, where the behavior is confirmed to be intentional: https://github.com/dotnet/runtime/issues/31167 –  Sep 08 '22 at 15:07

1 Answers1

1

Seeming as it is a mutable dictionary, just edit the keys, like so:

public static void RenameJsonExtensionDataPropertiesToCamelCase<T>( T objectValue )
{
    var jsonExtensionPropertyInfo = typeof(T)
        .GetProperties()
        .Select( p => ( pi: p, attrib: p.GetCustomAttribute<System.Text.Json.Serialization.JsonExtensionDataAttribute>() ) )
        .Where( t => t.attrib != null )
        .SingleOrDefault();

    if( jsonExtensionPropertyInfo.attrib is null ) return;

    PropertyInfo pi = jsonExtensionPropertyInfo.pi;
    
    Object? dictObj = pi.GetValue( objectValue );
    if( dictObj is Dictionary<String,Object?> dict )
    {
        RenameImpl( dict );
    }
    else if( dictObj is Dictionary<String,JsonElement?> jsDict )
    {
        RenameImpl( jsDict );
    }
    else
    {
        throw new NotImplementedException( "TODO?" );
    }
    
    //
    
    static void RenameImpl<TValue>( Dictionary<String,TValue> dict )
    {
        List<String> pascalCaseKeys = dict.Keys
            .Where( k => k.Length > 0 && Char.IsUpper( k[0] ) )
            .ToList();

        foreach( String pascalCaseKey in pascalCaseKeys )
        {
            String camelCaseKey = Char.ToLowerInvariant( pascalCaseKey[0] ) + pascalCaseKey.Substring( startIndex: 1 );
            dict[ camelCaseKey ] = dict[ pascalCaseKey ];
            dict.Remove( pascalCaseKey );
        }
    }
}

...then just call RenameJsonExtensionDataPropertiesToCamelCase right before you serialize:

var options = new JsonSerializerOptions
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNamingPolicy   = JsonNamingPolicy.CamelCase,
    DictionaryKeyPolicy    = JsonNamingPolicy.CamelCase,
};

var test = new Test
{
    Labels = new Dictionary<String,Object?>
    {
        { "Test1", new TestContent{ Value = "TestContent1" } }, 
        { "Test2", new TestContent { Value = null } },
    },
    MetaInfo = new Dictionary<String,Object?>
    {
        { "Test1", new TestContent{ Value = "TestContent1" } },
        { "Test2", new TestContent { Value = null } },
    }
};

RenameJsonExtensionDataPropertiesToCamelCase( test ); // <-- Here.

var result = JsonSerializer.Serialize(test, options);

System.Console.WriteLine(result);

Screenshot proof:

(Interestingly, after renaming Test1 and Test2 to test1 and test2 (respectively) .NET seems to serialize the keys in a different order).

enter image description here

Dai
  • 141,631
  • 28
  • 261
  • 374
  • Thanks. This approach seems that the key value of the dictionary is converted before the conversion. But I think the best answer may be is used `JsonConvert` to accomplish. – Changemyminds Sep 09 '22 at 02:49