Home > OS >  JsonConverter - WebApi - Case Sensitivity - Polymorphic
JsonConverter - WebApi - Case Sensitivity - Polymorphic

Time:05-20

I'm using a JsonConverter to deal with a polymorphic collection:

class ItemBatch
{
    List<ItemBase> Items { get; set; }
}

// For type discrimination of ItemBase
class ItemTypes
{
    public int Value { get; set; }
}

[JsonConverter(typeof(ItemConverter))]
abstract class ItemBase
{
    public abstract ItemTypes Type { get; set; }
}

class WideItem : ItemBase
{
    public override ItemTypes Type => 1;
    public decimal Width { get; set; }
}

class HighItem : ItemBase
{
    public override ItemTypes Type => 2;
    public decimal Height { get; set; }
}

class ItemConverter : JsonConverter<ItemBase>
{
    public override ItemBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (JsonDocument jsonDoc = JsonDocument.ParseValue(ref reader));

        int type = jsonDoc.RootElement.GetProperty("Type").GetProperty("Value").GetInt32();

        // deserialize depending on type.
    }
}

Am using Blazor and store the ItemBatch in IndexedDb before retrieving again later and sending to WebAPI.

Serializing to IndexedDb and deserializing from IndexedDb works fine.

But, when I try to send the ItemBatch to a WebAPI, I get the error:

Exception thrown: 'System.Collections.Generic.KeyNotFoundException' in System.Text.Json.dll An exception of type 'System.Collections.Generic.KeyNotFoundException' occurred in System.Text.Json.dll but was not handled in user code The given key was not present in the dictionary.

From peeking at various values, I suspected an issue with case-sensitivity. Indeed, if I change:

int type = jsonDoc.RootElement.GetProperty("Type").GetProperty("Value").GetInt32();

to

int type;

try
{
    type = jsonDoc.RootElement.GetProperty("Type").GetProperty("Value").GetInt32();
}
catch (Exception)
{
    type = jsonDoc.RootElement.GetProperty("type").GetProperty("value").GetInt32();
}

then I get past this error and my WebAPI gets called.

What am I missing that allows me to serialize / deserialize to / from IndexedDb, but the WebApi Json conversion is having issues with case sensitivity.

CodePudding user response:

Newtonsoft was case insensitive.

With System.Text.Json you have to pull some more levers.

https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-6-0#case-insensitive-deserialization

Case-insensitive deserialization During deserialization, Newtonsoft.Json does case-insensitive property name matching by default. The System.Text.Json default is case-sensitive, which gives better performance since it's doing an exact match. For information about how to do case-insensitive matching, see Case-insensitive property matching.

See this below URL as well:

https://makolyte.com/csharp-case-sensitivity-in-json-deserialization/

Here is a possible way to deal with it:

https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-character-casing

above url is ::: How to enable case-insensitive property name matching with System.Text.Json

https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsonelement.getproperty?view=net-6.0#system-text-json-jsonelement-getproperty(system-string)

Remarks
Property name matching is performed as an ordinal, case-sensitive comparison.

I don't think you can overcome that behavior on the "roll your own".

But maybe you can chase this:

https://docs.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.propertynamecaseinsensitive?view=net-6.0#system-text-json-jsonserializeroptions-propertynamecaseinsensitive

But that seems to be without "roll your own".

CodePudding user response:

My conclusion is that using default serialization, the JsonSerializerOptions do not set a value "CamelCase" for the PropertyNamingPolicy, but the HttpClient and WebApi Request Pipeline do:

PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

This means that when I'm serializing and deserializing to IndexedDb, the default serialization leaves the Json as Pascal Case.

My custom JsonConverter, which was using the options parameter passed into the Read and Write methods, therefore used Pascal case on the client when working with IndexedDb.

However, when the same JsonConverter is called by the HttpClient and the WebApi request pipeline, the options are set to:

PropertyNamePolicy = JsonNamingPolicy.CamelCase.

and when trying to parse to a JsonDocument, the content is now camel cased and my reading of the JsonDocument using Pascal case assumptions then fell over.

The answer to my problem was to update the write method as follows:

public override void Write(Utf8JsonWriter writer, SaleCommandBase value, JsonSerializerOptions options)
{
    JsonSerializerOptions newOptions = new JsonSerializerOptions(options) { PropertyNamingPolicy = null };

    JsonSerializer.Serialize(writer, (object)value, newOptions);
}

This forces the serialization to use Pascal case in all situations, whether that be local serialization on the client (when writing to IndexedDb) or whether serializing within the HttpClient when sending to a WebAPI.

Similary, in the read method:

    using (JsonDocument jsonDoc = JsonDocument.ParseValue(ref reader))
    {
        int type = jsonDoc.RootElement.GetProperty("Type").GetProperty("Value").GetInt32();

        newOptions = new JsonSerializerOptions(options) { PropertyNamingPolicy = null };

        return type switch
        {
            1 => jsonDoc.RootElement.Deserialize<WideItem>(newOptions),
            2 => jsonDoc.RootElement.Deserialize<HighItem>(newOptions),
            _ => throw new InvalidOperationException($"Cannot convert type '{type}'."),
        };
    }

By copying whatever the provided options are, but then overriding the naming policy to use Pascal case (PropertyNamingPolicy = null), then I can be assured that the Json document parsed will always be in Pascal case, regardless of the options provided by the framework.

  • Related