You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
JsonSerializerDefaults.Web sets NumberHandling = AllowReadingFromString. Under that setting, the int case of union IntOrString(int, string) also claims the JSON String token, so every string is rejected as ambiguous, including strings that are not numeric ("hello"). The same inputs deserialize fine with JsonSerializerOptions.Default.
This matters because the Web defaults are what ASP.NET Core (Minimal APIs and MVC) and System.Net.Http.Json use. The IntOrString example from the C# docs and the Unions and closed hierarchies in ASP.NET Core post therefore cannot be used as a request body in a default ASP.NET Core app.
Reproduction
usingSystem.Text.Json;usingSystem.Text.Json.Serialization;foreach(var(name,o)innew[]{("Default",JsonSerializerOptions.Default),("Web",JsonSerializerOptions.Web)})foreach(varjsoninnew[]{"42","\"hello\"","\"42\""}){try{Console.WriteLine($"{name,-7}{json,-8} => {JsonSerializer.Deserialize<IntOrString>(json,o).Value?.GetType().Name}");}catch(Exceptione){Console.WriteLine($"{name,-7}{json,-8} => {e.GetType().Name}: {e.Message}");}}try{JsonSerializer.Deserialize<IntOrStringStrict>("\"hello\"",JsonSerializerOptions.Web);}catch(Exceptione){Console.WriteLine($"[JsonNumberHandling(Strict)] on the union => {e.GetType().Name}");}publicunionIntOrString(int,string);[JsonNumberHandling(JsonNumberHandling.Strict)]publicunionIntOrStringStrict(int,string);
Output:
Default 42 => Int32
Default "hello" => String
Default "42" => String
Web 42 => Int32
Web "hello" => JsonException: JSON value type 'String' is ambiguous for union type 'IntOrString' because multiple case types can use this value type. Specify a custom type classifier to support deserialization. Path: $ | LineNumber: 0 | BytePositionInLine: 7.
Web "42" => JsonException: JSON value type 'String' is ambiguous for union type 'IntOrString' …
[JsonNumberHandling(Strict)] on the union => JsonException
Expected behavior
One of the following. I have listed them in order of preference.
An exact token-kind match wins. A String token binds to the string case when the union has one. Number-from-string coercion applies only when no case accepts the token natively. That would make Web agree with Default for all three inputs above.
[JsonNumberHandling] on the union type applies to its cases. Today it has no effect, so there is no per-type opt-out; the only fixes are app-wide options or a custom classifier.
If ambiguity here is intentional, document it. Unions whose cases are distinct JSON value kinds under Default stop being distinct under Web, so (int, string) needs a classifier in any ASP.NET Core app. Verify Minimal API unions support in RDF and RDG as endpoint parameters and return types aspnetcore#66951 appears to treat "ambiguous primitive cases without classifier → 400" as expected, but the docs and the blog post present IntOrString without that caveat.
Impact observed on RC 1
A Minimal API or MVC [ApiController] body of type IntOrString returns 400 for any string.
HttpClient.GetFromJsonAsync<IntOrString> throws the same JsonException on a string payload.
Microsoft.AspNetCore.OpenApi publishes the int arm as {"type":["integer","string"],"pattern":"^-?(?:0|[1-9]\\d*)$","format":"int32"} next to {"type":"string"}. The document therefore advertises string inputs that the server rejects.
Current workarounds are NumberHandling = Strict on the host's options (which changes number
parsing for the whole app, and must be set separately for Minimal APIs and MVC), or a custom JsonTypeClassifierFactory that maps JsonTokenType.String to typeof(string).
Description
JsonSerializerDefaults.WebsetsNumberHandling = AllowReadingFromString. Under that setting, theintcase ofunion IntOrString(int, string)also claims the JSON String token, so every string is rejected as ambiguous, including strings that are not numeric ("hello"). The same inputs deserialize fine withJsonSerializerOptions.Default.This matters because the Web defaults are what ASP.NET Core (Minimal APIs and MVC) and
System.Net.Http.Jsonuse. TheIntOrStringexample from the C# docs and the Unions and closed hierarchies in ASP.NET Core post therefore cannot be used as a request body in a default ASP.NET Core app.Reproduction
Output:
Expected behavior
One of the following. I have listed them in order of preference.
stringcase when the union has one. Number-from-string coercion applies only when no case accepts the token natively. That would makeWebagree withDefaultfor all three inputs above.[JsonNumberHandling]on the union type applies to its cases. Today it has no effect, so there is no per-type opt-out; the only fixes are app-wide options or a custom classifier.Defaultstop being distinct underWeb, so(int, string)needs a classifier in any ASP.NET Core app. Verify Minimal API unions support in RDF and RDG as endpoint parameters and return types aspnetcore#66951 appears to treat "ambiguous primitive cases without classifier → 400" as expected, but the docs and the blog post presentIntOrStringwithout that caveat.Impact observed on RC 1
[ApiController]body of typeIntOrStringreturns 400 for any string.HttpClient.GetFromJsonAsync<IntOrString>throws the sameJsonExceptionon a string payload.Microsoft.AspNetCore.OpenApipublishes theintarm as{"type":["integer","string"],"pattern":"^-?(?:0|[1-9]\\d*)$","format":"int32"}next to{"type":"string"}. The document therefore advertises string inputs that the server rejects.NumberHandling = Stricton the host's options (which changes numberparsing for the whole app, and must be set separately for Minimal APIs and MVC), or a custom
JsonTypeClassifierFactorythat mapsJsonTokenType.Stringtotypeof(string).Configuration
11.0.100-rc.1.26425.128,Microsoft.NETCore.App11.0.0-rc.1.26425.128LangVersionoverride (net11.0defaults to C# 15)Repro as a runnable project: https://github.com/robertodalmonte/csharp-unions-probed/tree/main/probes/QRepro1 (
dotnet run; the last line of its output is the[JsonNumberHandling]case this issue now tracks).