0

I'm migrating a project from Microsoft.AspNetCore.Mvc.NewtonsoftJson to System.Text.Json. I couldn't do this in .NET Core 3.1 due to dotnet/runtime#38056 as some of my models contain properties of type IDictionary<int, T>, something like:

public class MyClass {
    public int ID { get; set; }
    public IDictionary<int, ThingClass> Things { get; set; } 
}

In .NET 5 this now works for serialisation, so this works:

[HttpGet("{id}")]
public async Task<MyClass> Get(int id) ...

However, when I send data back:

[HttpPost]
public async Task Update([FromBody] MyClass changes) ...

With a body like (note that the key of "things" is numbers, not strings}:

{ 
    "id": 789,
    "things": {
        123: { ... },
        456: { ... }
    }
}

I get an error:

Unable to cast object of type 'System.Collections.Generic.Dictionary`2[System.String,MyNamespace.ThingClass]' to type 'System.Collections.Generic.IDictionary`2[System.Int32,MyNamespace.ThingClass]'.

   at System.Text.Json.Serialization.Converters.IDictionaryOfTKeyTValueConverter`3.Add(TKey key, TValue& value, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.Converters.DictionaryDefaultConverter`3.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, TCollection& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.JsonPropertyInfo`1.ReadJsonAndSetMember(Object obj, ReadStack& state, Utf8JsonReader& reader)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCoreAsObject(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadCore[TValue](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
   at System.Text.Json.JsonSerializer.ReadAsync[TValue](Stream utf8Json, Type returnType, JsonSerializerOptions options, CancellationToken cancellationToken)

This looks like something is pre-parsing the body first (and assuming the key is a string) and then the step from Dictionary<string, ThingClass> to IDictionary<int, ThingClass> fails, but then the keys should never have been parsed as string in the first place.

What is doing this deserialisation wrong?

How do I configure what [FromBody] uses? Why isn't it using the same settings as the deserialisation?

Is this a bug in .NET 5's System.Text.Json (it seems way too basic, someone would have spotted it way before this) and if so is there any way around it?


Note: This question is not about whether numeric keys are in the RFC8259 standard (they aren't). They do work in actual JS and are in lots of JSON in the wild and so do sometimes need to be output and parsed. System.Text.Json claims that it supports non-string dictionary keys, I want to get that working. "Use NewtonsoftJson" or "you're not spec compliant" will not be accepted as answers.

Community
  • 1
  • 1
Keith
  • 150,284
  • 78
  • 298
  • 434

1 Answers1

0

It is not supported by System.Text.Json. According to the .NET 5.0 System.Text.Json migration docs:

System.Text.Json only accepts property names and string values in double quotes because that format is required by the RFC 8259 specification and is the only format considered valid JSON.

Here is the corresponding section of RFC 8259:

An object structure is represented as a pair of curly brackets surrounding zero or more name/value pairs (or members). A name is a string.

The fix for non-string dictionaries which you have linked seems to target version 6.0 of System.Text.Json so it seems that if you need to support this scenario you still need to use Newtonsoft.Json.

Community
  • 1
  • 1
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • Cheers, always super helpful to hear exactly which part of an abstract spec a current working application is breaking. I'm currently using Newtonsoft, it works because the JSON works (whether it meets the retroactively applied spec or not), this question is how do I upgrade? Thanks for your time, but this quite specifically does not answer the question. – Keith Nov 19 '20 at 20:00
  • @Keith Sorry, missed that you are trying to migrate from Newtonsoft. .NET 5.0 (as written in the linked docs for 5.0) still does not support non-string property names (and non-strings dictionaries). The bug fix link you've added targets `System.Text.Json - 6.0` which will be shipped with .NET 6.0 I assume. – Guru Stron Nov 19 '20 at 20:08
  • Hrm , good point, only it's merged into the 5.0 RC code and `Dictionary` works now in both serialisation and deserialisation if I call it directly. It seems like `System.Text.Json.JsonSerializer.Deserialize>` works in .NET 5, but doesn't in `[FromBody]`. – Keith Nov 19 '20 at 20:14
  • 1
    @Keith it [does not](https://dotnetfiddle.net/lBpyTC) for me. – Guru Stron Nov 19 '20 at 20:23
  • Woa, that is weird. Manually deserialising does work for my current code, but the `IDictionary` is a sub property of a class that is being parsed. I'll try unpicking that and see if I can get something that either a) works, or b) crashes in the same way your sandbox is. – Keith Nov 19 '20 at 20:33
  • 1
    @Keith TBH moving to the sub property [does not change anything](https://dotnetfiddle.net/geiisF). Both on my machine and in the fiddle. – Guru Stron Nov 19 '20 at 21:15
  • 1
    And right from the [docs](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#table-of-differences-between-newtonsoftjson-and-systemtextjson): ***Table of differences between Newtonsoft.Json and System.Text.Json**: Allow property names without quotes ❌ Not supported*. – dbc Nov 19 '20 at 22:43
  • 1
    @Keith - To confirm what this answer says, JSON with unquoted property names is malformed according to the [standard](https://json.org/) and always have been. If you upload your JSON to https://jsonlint.com/ or https://jsonformatter.curiousconcept.com/ you will get an error. Json.NET will parse nonquoted property names because it supports this extension to the standard, see [How to enforce quotes on property names on JSON .NET](https://stackoverflow.com/q/53304218/3744182) and [Support "strict mode" for RFC7159 parsing #646](https://github.com/JamesNK/Newtonsoft.Json/issues/646). – dbc Nov 19 '20 at 22:51
  • @dbc yes, there's JSON, as in JS object notation that works in JS, and the sub-set-JSON standard that adds restrictions for what I'm sure are good reasons (but I do hate not being able to comment in `.json` config files). Being strictly compliant with any standard is not a concern of my current project, working in browsers is. `NewtonsoftJson` works now, .NET 5 release hype goes "hey `System.Text.Json` is better", this question is how do I get the recommended solution to work in my existing project? "Your project is not compliant with a spec" is not an answer. – Keith Nov 20 '20 at 16:32
  • @dbc also, those docs refer to [**string** property names without quotes](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#json-strings-property-names-and-string-values), i.e. `{"prop":1}` is fine while `{prop:1}` is bad. In fact it states that [serialising a dictionary with a non-string key is **supported**](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#dictionary-with-non-string-key) – Keith Nov 20 '20 at 16:36
  • @GuruStron Even weirder - that is _working in my code_! However, I'm passing a load of settings that may change the behaviour from the default (none actually refer to dictionary serialisation though). I'll try changing/removing them and see if I can find which settings mean I'm seeing a different result. – Keith Nov 20 '20 at 16:43
  • @Keith - Serializing a dictionary with a non-string key is supported by converting the key to a string. See https://dotnetfiddle.net/twennS. That's because the JSON standard states that all property names are strings, see https://www.json.org/json-en.html, specifically https://www.json.org/img/object.png. If this is somehow working in your code then somewhere in your code path the JSON is getting preprocessed and normalized. – dbc Nov 20 '20 at 18:02