-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Original proposal by @NickCraver (click to view)
Apologies if this issue exists already...I hunted and couldn't find one.
I've been trying to switch a lot of usages over to System.Text.Json from Newtonsoft and Jil, but a recurring theme is handling external APIs and their not-quite-right returns of JSON.
Unfortunately, an example I'm seeing over and over is:
{ "field":"12345" }They're numbers, but as strings. int, double, decimal, whatever. It happens all the time. And since it's someone's API, it's unlikely we'll get the world to fix this.
Note: this is something Newtonsoft handles by default, which is why I don't think most people realize it's an issue.
Is there any chance we can let System.Text.Json handle number types being quoted when deserializing? Something a user opts-into via JsonSerializerOptions?
Small repro example:
void Main()
{
JsonSerializer.Parse<Foo>(@"{""Bar"":""1234""}");
}
public class Foo
{
public int Bar { get; set; }
}Edited by @layomia:
Users want to be able to deserialize JSON strings into number properties. Scenarios include deserializing "NaN", "Infinity" and "-Infinity" (#31024).
We should add an option to enable this scenario, and possibly allow serializing numbers as strings.
These semantics allow better interop with various API endpoints accross the web.
API Proposal
namespace System.Text.Json
{
public partial sealed class JsonSerializerOptions
{
public JsonNumberHandling NumberHandling { get; set; }
}
}
namespace System.Text.Json.Serialization
{
[Flags]
public enum JsonNumberHandling : byte
{
/// <summary>
/// No specified number handling behavior. Numbers can only be read from <see cref="JsonTokenType.Number"/> and will only be written as JSON numbers (without quotes).
/// </summary>
None = 0x0,
/// <summary>
/// Numbers can be read from <see cref="JsonTokenType.String"/>. Does not prevent numbers from being read from <see cref="JsonTokenType.Number"/>.
/// </summary>
AllowReadingFromString = 0x1,
/// <summary>
/// Numbers will be written as JSON strings (with quotes), not as JSON numbers.
/// </summary>
WriteAsString = 0x2,
/// Floating point constants represented as <see cref="JsonTokenType.String"/> tokens
/// such as "NaN", "Infinity", "-Infinity", can be read when reading, and such CLR values
/// such as <see cref="float.NaN"/>, <see cref="double.PositiveInfinity"/>, <see cref="float.NegativeInfinity"/> will be written as their corresponding JSON string representations.
AllowNamedFloatingPointLiterals = 0x4
}
public partial sealed class JsonNumberHandlingAttribute : JsonAttribute
{
public JsonNumberHandling Handling { get; }
public JsonNumberHandlingAttribute(JsonNumberHandling handling)
{
Handling = handling;
}
}
}Usage
Allow reading numbers from strings; write numbers as strings.
public class ClassWithInts
{
public int NumberOne { get; set; }
public int NumberTwo { get; set; }
}
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString
};
string json = @"{""Number1"":1,""Number2"":""2""}";
ClassWithInts @class = JsonSerializer.Deserializer<ClassWithInts>(json, options);
Console.WriteLine(@class.NumberOne); // 1
Console.WriteLine(@class.NumberTwo); // 2
json = JsonSerializer.Serialize(@class, options);
Console.WriteLine(json); // @"{""Number1"":""1"",""Number2"":""2""}";Allow reading numbers from floating point constants; write floating point constants
Given a class:
public class ClassWithNumbers
{
public int IntNumber { get; set; }
public float FloatNumber { get; set; }
}Without the new option, reading floating-point-constant representations fails:
string json = @"{""IntNumber"":1,""FloatNumber"":""NaN""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json);
// Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Single. Path: $.FloatNumber | LineNumber: 0 | BytePositionInLine: 34.Writing also fails:
var obj = new ClassWithNumbers
{
IntNumber = -1,
FloatNumber = float.NaN
};
string json = JsonSerializer.Serialize(obj);
// Unhandled exception. System.ArgumentException: .NET number values such as positive and negative infinity cannot be written as valid JSON.With the new option, reading floating-point-constant representations works:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
};
string json = @"{""IntNumber"":1,""FloatNumber"":""NaN""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
Console.WriteLine(obj.IntNumber); // 1
Console.WriteLine(obj.FloatNumber); // NaNWriting floating-point-constant representations also works:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
};
var obj = new ClassWithNumbers
{
IntNumber = -1,
FloatNumber = float.NaN
};
string json = JsonSerializer.Serialize(obj, options);
Console.WriteLine(json); // {"IntNumber":-1,"FloatNumber":"NaN"}Allow reading numbers from string; support reading and writing floating point constants
Reading:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals
};
string json = @"{""IntNumber"":""1"",""FloatNumber"":""NaN""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
Console.WriteLine(obj.IntNumber); // 1
Console.WriteLine(obj.FloatNumber); // NaNWriting:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals
};
var obj = new ClassWithNumbers
{
IntNumber = -1,
FloatNumber = float.NaN
};
string json = JsonSerializer.Serialize(obj, options);
Console.WriteLine(json); // {"IntNumber":-1,"FloatNumber":"NaN"}Write numbers as strings; support reading and writing floating point constants
Reading:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals
};string json = @"{""IntNumber"":""1""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
Console.WriteLine(obj.IntNumber); // 1
Console.WriteLine(obj.FloatNumber); // 0string json = @"{""IntNumber"":""1"",""FloatNumber"":""NaN""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
// Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Int32. Path: $.IntNumber | LineNumber: 0 | BytePositionInLine: 16.Writing:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowNamedFloatingPointLiterals
};
var obj = new ClassWithNumbers
{
IntNumber = -1,
FloatNumber = float.NaN
};
string json = JsonSerializer.Serialize(obj, options);
Console.WriteLine(json); // {"IntNumber":"-1","FloatNumber":"NaN"}Read semantics are the same as the serializer's default (no quotes)
For example, integers cannot have decimal places:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString
}
string json = @"{""IntNumber"":""1.0""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
// Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Int32. Path: $.IntNumber | LineNumber: 0 | BytePositionInLine: 17.Similarly, there can't be any leading or trailing trivia:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString
}
string json = @"{""IntNumber"":""1a""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
// Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Int32. Path: $.IntNumber | LineNumber: 0 | BytePositionInLine: 15.Leading or trailing whitespace is also not allowed:
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowReadingFromString
}
string json = @"{""IntNumber"":""1 ""}";
ClassWithNumbers obj = JsonSerializer.Deserialize<ClassWithNumbers>(json, options);
// Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to System.Int32. Path: $.IntNumber | LineNumber: 0 | BytePositionInLine: 15.Note that formats such as the currency and percentage formats are not allowed. Commas (,), the percent symbol (%), and underscores (_) are not permitted in input strings.
Notes
Newtonsoft compat
- Newtonsoft.Json allows implicit conversions from strings to numbers on deserialization, i.e, no custom logic or setting is required. We will require that users explictly opt in to this behavior.
- Newtonsoft.Json does not provide a built-in way to serialize numbers as strings. Custom logic, e.g. a custom converter needs to provided for this behavior. This proposal includes an easy way to specify this behavior.
Read/write semantics
- The number reading and writing behavior of this feature is exactly the same as if no quotes were specified, except that surrounding quotes can be allowed on deserialization, and can be written on serialization.
- Reading/writing is not culture aware (i.e InvariantCulture). A custom converter has to be specified for culture-aware handling.
- The "G17" standard numeric format is used when reading and writing
doublerepresentations. - The "G9" standard format is used when reading and writing
floatrepresentations. - Percentage and currency formats are not supported.
- Numbers represented as
JsonTokenType.Stringtokens will be unescaped if needed when deserializing. This is in keeping with other string-based parsing for types likeDateTimeandGuid. - Only the literal "NaN", "Infinity", and "-Infinty" values are allowed. This is in keeping with the most common variations on the web. The matching is case-sensitive. We can be more permissive in the future, based on user feedback.