Repository navigation
System.Text.Json deserializing of object[bool] does not produce boolean #29960
Description
Activity
Current functionality treats any
objectparameter asJsonElementwhen deserializing. The reason is that we don't know what CLR type to create, and decided as part of the design that the deserializer shouldn't "guess".For example, a JSON string could be a DateTime but the deserializer doesn't attempt to inspect. For a "True" or "False" in JSON that is fairly unambiguous to deserialize to a Boolean, but we don't since we don't want to special case String or Number, we don't want to make an exception for True or False.
With the upcoming preview 7, it is possible to write a custom converter for
objectthat changes that behavior. Here's a sample converter:public class ObjectBoolConverter : JsonConverter<object> { public override object Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.True) { return true; } if (reader.TokenType == JsonTokenType.False) { return false; } // Forward to the JsonElement converter var converter = options.GetConverter(typeof(JsonElement)) as JsonConverter<JsonElement>; if (converter != null) { return converter.Read(ref reader, type, options); } throw new JsonException(); // or for best performance, copy-paste the code from that converter: //using (JsonDocument document = JsonDocument.ParseValue(ref reader)) //{ // return document.RootElement.Clone(); //} } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { throw new InvalidOperationException("Directly writing object not supported"); } }
Used like
var options = new JsonSerializerOptions(); options.Converters.Add(new ObjectConverter()); object boolObj = JsonSerializer.Parse<object>("true", options); bool b = (bool)boolObj; Debug.Assert(b == true); object elemObj = JsonSerializer.Parse<object>(@"{}", options); Debug.Assert(elemObj is JsonElement);
Reacted by Ahson Khan and Ganbarukamo41Reacted by Renato, Nathan Hollis, Shaun Tonstad and John Carewsounds good, I'll use the custom converter. Thanks.
@steveharter Json and Javascript don't have many type, but
booleanis known one.
System.Text.Jsonshould map json known'd type to clr type directly without custom converterjson clr boolean bool number double string string null null undefinednull Reacted by John Carew@John0King I provided the rationale previously. Automatically mapping number to double won't work for large decimals.
What are the scenarios for using system.object instead of the strong type (double, string, bool)?
FWIW, using an
object/dynamicproperty for message deserialization is a security consideration and should be handled with care:Reacted by Renato and Mathieu Malaterre@steveharter
you are right, In many scenarios , handleJsonElementis much easier than an unknow/dynamic object type . it's just different than Json.Net that will break many peopleMaybe it's better to throw instead of deserializing as
JsonElement. That way the decision can still be made in the future on how to deserialize to typeobject. Now, we're locked in toJsonElement.I have personally wanted to deserialize to object a few times in the past with JSON.NET. I always wanted the "obvious" deserialization to
booletc.If I want to deserialize to
JsonElementthen I can make the property aJsonElement.@steveharter
So, regarding dictionary<string,object>, what is this supposed to look like? The following throws an exception.if (reader.TokenType == System.Text.Json.JsonTokenType.StartObject) { // https://github.com/dotnet/corefx/issues/39953 // https://github.com/dotnet/corefx/issues/38713 var conv = options.GetConverter(typeof(Dictionary<string, object>)) as System.Text.Json.Serialization.JsonConverter<Dictionary<string, object>>; if (conv != null) { return conv.Read(ref reader, type, options); } throw new System.Text.Json.JsonException(); }Regarding the design choices;
What can be used to represent an Object other than dictionary?
What can be used to represent an Array other than array?I can appreciate abstraction, but somebody has the make the final implementation decision. The current design completely ignores people who happened to stumble upon a Json file as a job requirement, and most likely need to use Json and System.Text.Json once and never again. For those folks, something that reasonably easily generates dictionarys and arrays and strings (Json files are one big string, right?) with little effort (such as System.Web.Script.Serialization.JavaScriptSerializer), and some (any?) reasonable default deserialization decisions/assumptions would make life so much easier.
I don't really like this design. You are making the most common use cases for a parser an impenetrable mess to serve the general correctness. To serve the 2% edge cases, you are making this yet another unusable .NET Json API for the 98% use cases. You have an options bucket, use it to OPT into the generally correct, but unhelpful behavior, not have it be the default.
Reacted by Corys, GSPP, RandomGHUser, Mina, Alexandr Gilevich, Stian Sandve, Piotr Karczmarz, thinkards, Soheil Alizadeh, Mikkel Hansen and 8 moreReacted by Shaun Tonstad@gmurray81 can you explain your scenario for using System.Object (instead of just a bool) and what other values it may hold other than a bool?
Since JsonElement would still be used for non-bool types (e.g. objects, arrays, numbers) how would having a special case for
boolmake the code easier to consume?In most cases when I've seen this, it has been an anti-pattern.
objectis rarely what people want, and have seen far too manymodel.Foo.ToString()from people who didn't realize they shouldn't be usingobject.Reacted by Steve Harter and yurriiyReacted by Daniel Gerlag, Renato and Shaun TonstadThe JSON->POCO auto-generator in VS creates
objectmembers when it seesnull, so this behavior breaks anyone migrating from such a thing.But in most cases, this is again people just letting the default give them a bad model when they should be fixing what it gives them.
Reacted by Steve Harter and yurriiysuggest to add a new strategy :
PreferRawObject:trueto use default object mappingjson clr boolean bool number double string string null null undefinednull Date(string with TimezoneInfo) DateTimeOffset Date(string without TimezoneInfo ) DateTime Reacted by Nemanja Đorđević, moander and Shaun TonstadReacted by Shaun TonstadReacted by Shaun Tonstadsuggest to add a new strategy :PreferRawObject:true to use default object mapping
Yes some global option is doable.
However, it is actually fairly easy to create a custom converter to handle the cases you show above. Earlier I provided the sample for
booland here's a sample forbool,double,string,DateTimeOffsetandDateTime. I will get the sample added toruntime/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Object.cs
Line 267 in 3e4a06c
private class SystemObjectNewtonsoftCompatibleConverter : JsonConverter<object> private class SystemObjectNewtonsoftCompatibleConverter : JsonConverter<object> { public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.True) { return true; } if (reader.TokenType == JsonTokenType.False) { return false; } if (reader.TokenType == JsonTokenType.Number) { if (reader.TryGetInt64(out long l)) { return l; } return reader.GetDouble(); } if (reader.TokenType == JsonTokenType.String) { if (reader.TryGetDateTime(out DateTime datetime)) { return datetime; } return reader.GetString(); } // Use JsonElement as fallback. // Newtonsoft uses JArray or JObject. using (JsonDocument document = JsonDocument.ParseValue(ref reader)) { return document.RootElement.Clone(); } } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { throw new InvalidOperationException("Should not get here."); } }
Reacted by Mani Gandham, Nick Albrecht, Andy, Matz Wiik, Matias Quaranta, Dan Soper, Mahdi Kouhestani, IchHabeKeineNamen, asmolarczyk and Mathieu MalaterreReacted by Shaun TonstadSee the
SystemObjectNewtonsoftCompatibleConvertersample class infor semantics similar to Json.NET (except for objects and arrays).runtime/src/libraries/System.Text.Json/tests/Serialization/CustomConverterTests.Object.cs
Line 267 in 7eea339
private class SystemObjectNewtonsoftCompatibleConverter : JsonConverter<object> Reacted by Nick Albrecht and Matz Wiik9 remaining items
Is there a concrete solution for the controllers in ASP.NET Core? The
objectin parameterIDictionary<string, object> requestis alwaysJsonElement, which does not support a typecast.Update
The solution is to set this line to
startup.cs:services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add(new SystemObjectNewtonsoftCompatibleConverter()) );
SystemObjectNewtonsoftCompatibleConverteris copied from the code in the previous discussions.After that, the
objectin the parameter will be castable:[HttpPost("Post")] public async Task<IActionResult> PostAsync(IDictionary<string, object> requestModel) { // ... }As mentioned above, the current design avoids deserializing JSON string and bool to CLR string and bool because this would not be consistent with how JSON numbers are deserialized (since we don't know the CLR number type and don't want to "guess").
Imagine having a CLR
object[]property and if the JSON has mixed values (strings, bools, numbers), some array elements would be are CLRstring, somebooland the numbers would beJsonElement-- that would be more confusing IMHO. Also, Newtonsoft deserializes JSONstringsometimes as a GUID or DateTime, which can also be confusing and\or unexpected.So if we want to add a Newtonsoft custom converter that is possible, but we can't change the default behavior in any case since that would break backwards compat.
Reacted by John CarewPer comments above, I'll close this issue. Changing the default behavior would be a breaking change, and there are workarounds described in System.Text.Json documentation: https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0#deserialize-inferred-types-to-object-properties.
@steveharter there was a few shared examples in these comments that gave you great examples of how to test if a JavaScript Number type can safely be deserialized as a valid CLR type.
As for the bool type you keep through into the mix with Number, what other bool type is there in JavaScript and CLR other than Bool? Why are you not at least converting bool types to valid CLR bool type? I can somewhat see your point about Number type; even though there is simple ways to test for valid serialization, but I don't get the reason for not allowing to do one-to-one bool type.
@johnwc can you share some scenarios where the CLR type is
System.Objectfor numbers and why it can't be strongly typed asSystem.DecimalorSystem.Double?JavaScript Number type can safely be deserialized as a valid CLR type.
If the only known JSON producer\consumer is JavaScript, then I recommend using a custom converter that converts to
doubleonly.The two main issues with using
doublefor JSON numbers:- Loss of precision when dealing with multiple monetary values (such as adding several values and doing an
Equals()-- example below). - Very large or small integers should be converted to
decimalorBigNumberor lose precision.
Neither
Utf8JsonReaderorJsonElementautomatically perform JSON number conversions (it leaves it up to the user to explicitly callGetDouble(), etc), so the serializer follows that same logic and doesn't "guess". However, it does support using a custom converter (in a relatively easy manner) to do this in whatever fashion is appropriate.To avoid precision loss when dealing with monetary values,
decimalshould be used, notdouble. If JSON numbers are converted todouble(when the CLR type is unknown viaSystem.Object) that means the corresponding user logic will need to usedoublewhich may cause various precision issues:using System; public class Example { public static void Main() { Double[] values = { 10.0, 2.88, 2.88, 2.88, 9.0 }; Double result = 27.64; Double total = 0; foreach (var value in values) total += value; if (total.Equals(result)) Console.WriteLine("The sum of the values equals the total."); else Console.WriteLine("The sum of the values ({0}) does not equal the total ({1}).", total, result); } } // The example displays the following output: // The sum of the values (27.639999999999997) does not equal the total (27.64).
Note Newtonsoft behavior; not exactly intutitive IMHO:
object oDouble = JsonConvert.DeserializeObject("1.0"); Console.WriteLine(oDouble.GetType()); // "System.Double object oLong = JsonConvert.DeserializeObject("1"); Console.WriteLine(oLong.GetType()); // System.Int64 object oBigInteger = JsonConvert.DeserializeObject(decimal.MaxValue.ToString()); Console.WriteLine(oBigInteger.GetType()); // System.Numerics.BigInteger
- Loss of precision when dealing with multiple monetary values (such as adding several values and doing an
@steveharter again, you keep focusing on doubles and Number type. Use decimals... or use doubles... but not to some arbitrary JsonElement object. It really does not matter what it converts it to, when there is a way to have the ability to overwrite it for exactly how you need it to be outside of the default. Truthfully, I think you're getting caught up on something that is not really an issue for 99% of use cases. As most will not be using an object type for when needing to do math or working with monetary values. With the way it is now, you are forcing the 99% to now write more custom functionality, instead of just using OoB functionality.
Can you please answer the main part of the comment that was about Bool types not being deserialized?
Can you please answer the main part of the comment that was about Bool types not being deserialized?
Here's what I mentioned prior:
Imagine having a CLR object[] property and if the JSON has mixed values (strings, bools, numbers), some array elements would be are CLR string, some bool and the numbers would be JsonElement -- that would be more confusing IMHO
Thus if we auto-convert JSON bool to
System.Booleanthe doc and behavior would be somewhat ambiguous: "When the CLR type isSystem.Objectthen all JSON values are converted toJsonElementexcept a JSON bool which is converted toSystem.Boolean".And if we do that one-off behavior, the next question is what about JSON string (
System.String), JSON arrays (System.Array), JSON objects (Dictionary<string, object>) and then of course numbers.Going forward, since we can't break default behavior, here are options to address:
- Add values to the
JsonUnknownTypeHandlingenum (which is set onJsonSerializerOptionsto make this easier such as:
JsonUnknownTypeHandling.AutoPrimitivesThenJsonElement
JsonUnknownTypeHandling.AutoPrimitivesThenJsonElement - Provide a "SystemObjectPrimitiveConverter" or "SystemObjectNewtonsoftCompat" custom converter that must be manually added to
JsonSerializerOptions. - Do nothing; users add an appropriate custom converter either from samples or hand-written based on requirements.
- Add values to the
FWIW the new
JsonNodemay help in certain scenarios since it at least allows explicit casts to primitives:// If you can use JsonNode\JsonArray in the signature: JsonArray nodes = JsonSerializer.Deserialize<JsonArray>("[1,1.1,\"Hello\"]"); long l = (long)nodes[0]; double d = (double)nodes[1]; string s = (string)nodes[2];
If you must have
System.Object, you can still leverage nodes:JsonSerializerOptions options = new(); options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode; object[] objects = JsonSerializer.Deserialize<object[]>("[1,1.1,\"Hello\"]", options); long l = (long)(JsonNode)objects[0]; // or alternatively: l = ((JsonValue)objects[0]).GetValue<long>(); double d = (double)(JsonNode)objects[1]; string s = (string)(JsonNode)objects[2];
Reacted by Andreas DirnbergerI was under the assumption that strings were actually being deserialized as CLR strings, and that only bool and Number types were for some reason being left as JsonElement.
Is there a reason for that
JsonElementhas no explicit cast operators implemented?Per comments above, I'll close this issue. Changing the default behavior would be a breaking change
In fact , I think you can not break anyone who use .net to build their application, because no one use
System.Text.Json,
unless, they are writing demos 🤣.and for the scenario that use
System.Text.Jsonin Authentication, well , you already break us before , why you so afraid to break us again ? and beside , break change comes anyway , if it's not here, then it will be there .so breaking change should not effect what we talk here, we should talk about what does it should be (the best practice)
so breaking change should not effect what we talk here, we should talk about what does it should be (the best practice)
Best practice IMO is to be explicit on the CLR type. This mean either:
- Change the consuming code to use an explicit type and not
System.Object. This avoids the ambiguity (and is faster). - Use the current explicit behavior of
JsonElement\JsonNodeto obtain the appropriate CLR type from the JSON primitive, possibly calling helper methods to parse the string if the Type is not directly supported: - JSON bool: Boolean
- JSON string: String, DateTime, Guid, Uri, etc
- JSON number: Long, Ulong, Decimal, etc
- Change the consuming code to use an explicit type and not
- ghost locked as resolved and limited conversation to collaborators
on Aug 11, 2021
I understand that the System.Text.Json is still in development. However, I would like to point out that the new deserializer produces very different results than the previous one.
Scenario:
Controller method:
Posting this json:
{ "ParamName": "Bool param", "ParamValue": false }In .net core 2.1, the false value deserializes as boolean:
Type of 'Bool param' is System.Boolean; value is False
However, in .net core 3.0 preview 6, the false value deserializes as System.Text.Json.JsonElement:
Type of 'Bool param' is System.Text.Json.JsonElement; value is False
Will there be any chance to make the new deserializer work the same as in 2.1?
Note: we declare the ParamValue as object, as in the real app the values are of several different types, and so far the deserializer handled all them for us without issues. In 3.0, all this functionality is broken.
Thanks.