C# – Deserializing JSON with quoted numbers

There are two ways to represent numbers in JSON: as number literals (ex: 123) or as quoted numbers (ex: “123”). In this article, I’ll explain how quoted numbers are handled during deserialization in Newtonsoft and System.Text.Json and how to change the behavior. At the end, I’ll show how to write quoted numbers during serialization.

Quoted number handling in Newtonsoft

By default, Newtonsoft handles both literal numbers and quoted numbers. It tries to convert quoted numbers to the appropriate target type.

Here’s an example. Let’s say we want to deserialize the following JSON:

{
  "title": "Dune",
  "yearReleased": "2021",
  "score": 8.4
}
Code language: JSON / JSON with Comments (json)

Here’s the code:

using Newtonsoft.Json;

var movie = JsonConvert.DeserializeObject<Movie>(movieJson);

Console.WriteLine($"Year={movie.YearReleased} Score={movie.Score}");
Code language: C# (cs)

This outputs the following:

Year=2021 Score=8.4Code language: plaintext (plaintext)

It was able to handle the number literal (8.4) and the quoted number (“2021”).

Handling quoted decimal values

Decimal formats are based on culture. Let’s say you’re deserializing the following JSON with revenue in the Spanish (es-ES) format:

{
  "title": "Dune",
  "revenue":"374.232.547,12"
}
Code language: JSON / JSON with Comments (json)

Newtonsoft would throw the following exception:

Newtonsoft.Json.JsonReaderException: Could not convert string to decimal: 374.232.547,12

One way to deal with this is to set the culture. When it tries to convert the quoted number (“374.232.547,12”), it’ll use the specified culture:

using Newtonsoft.Json;

var movie = JsonConvert.DeserializeObject<Movie>(movieJson,
	new JsonSerializerSettings()
	{
		Culture = System.Globalization.CultureInfo.GetCultureInfo("es-ES")
	});

Console.WriteLine($"Year={movie.YearReleased} Revenue={movie.Revenue}");
Code language: C# (cs)

This enables Newtonsoft to correctly handle the quoted decimal value. This outputs the following:

Year=2021 Revenue=374232547.12Code language: plaintext (plaintext)

Quoted number handling in System.Text.Json

By default, System.Text.Json uses strict number handling, which means it only handles number literals (ex: 123). When it encounters a quoted number, it throws an exception.

Here’s an example. Let’s say we want to deserialize the following JSON with a quoted number:

{
  "title": "Dune",
  "yearReleased": "2021",
  "score": 8.4
}
Code language: JSON / JSON with Comments (json)

Here’s the code (using the default deserialization options):

using System.Text.Json;

var movie = JsonSerializer.Deserialize<Movie>(movieJson);

Console.WriteLine($"Year={movie.YearReleased} Score={movie.Score}");
Code language: C# (cs)

Because it’s using strict number handling, it throws the following exception when it runs into the quoted number:

System.Text.Json.JsonException: The JSON value could not be converted to System.Int32

This default behavior can be changed. How you change it depends on which version of .NET you’re using.

Changing the quoted number handling in .NET 5 and above

Starting in .NET 5, you can set the NumberHandling setting to JsonNumberHandling.AllowReadingFromString to make it handle both number literals and quoted numbers.

using System.Text.Json;
using System.Text.Json.Serialization;

var jsonOptions = new JsonSerializerOptions()
{
	NumberHandling = JsonNumberHandling.AllowReadingFromString
};

var movie = JsonSerializer.Deserialize<Movie>(movieJson, jsonOptions);

Console.WriteLine($"Year={movie.YearReleased} Score={movie.Score}");
Code language: C# (cs)

This outputs the following:

Year=2021 Score=8.4Code language: plaintext (plaintext)

Changing the settings per property

You can apply the JsonNumberHandling attribute to properties to change the settings per property. This overrides JsonSerializerOptions.NumberHandling.

For example, let’s say you only want to allow relaxed number handling for the Movie.Score property:

using System.Text.Json.Serialization;

class Movie
{
	public string Title { get; set; }
	public int YearReleased { get; set; }
	public decimal Revenue { get; set; }

	[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
	public decimal Score { get; set; }
}
Code language: C# (cs)

Here’s the JSON:

{
  "title": "Dune",
  "yearReleased": 2021,
  "score": "8.4"
}Code language: JSON / JSON with Comments (json)

Here’s the code. It’s using default number handling (strict):

using System.Text.Json;

var jsonOptions = new JsonSerializerOptions();

var movie = JsonSerializer.Deserialize<Movie>(movieJson, jsonOptions);

Console.WriteLine($"Year={movie.YearReleased} Score={movie.Score}");
Code language: C# (cs)

This outputs the following:

Year=2021 Score=8.4

It allowed the quoted number for the Movie.Score property and converted “8.4” to the target type.

Handling quoted decimal values

System.Text.Json can’t handle quoted decimal values in non-default formats. For example, let’s say we want to deserialize the following with the revenue shown in the Spanish (es-ES) decimal format:

{
  "Title": "Dune",
  "Revenue":"374.232.547,12"
}Code language: JSON / JSON with Comments (json)

System.Text.Json will throw the following exception during deserialization:

System.Text.Json.JsonException: The JSON value could not be converted to System.Decimal

Unlike Newtonsoft, there doesn’t seem to be an easy way to set the culture or even specify the decimal format (at least not at the time of this writing). Therefore, you have to write a custom JSON converter that handles converting the quoted decimal value using the specified culture.

Here’s an example of culture-specific quoted decimal converter:

using System.Text.Json;
using System.Text.Json.Serialization;

public class CultureSpecificQuotedDecimalConverter : JsonConverter<decimal>
{
	public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (reader.TokenType == JsonTokenType.String)
		{
			return Convert.ToDecimal(reader.GetString(), System.Globalization.CultureInfo.GetCultureInfo("es-ES"));
		}
		else
		{
			return reader.GetInt32();
		}
	}

	//Write() not shown
}
Code language: C# (cs)

Use it like this:

using System.Text.Json;

var jsonOptions = new JsonSerializerOptions();
jsonOptions.Converters.Add(new CultureSpecificQuotedDecimalConverter());

var movie = JsonSerializer.Deserialize<Movie>(movieJson, jsonOptions);

Console.WriteLine($"Year={movie.YearReleased} Revenue={movie.Revenue}");
Code language: C# (cs)

This outputs the following:

Year=2021 Revenue=374232547.12Code language: plaintext (plaintext)

Changing the quoted number handling before .NET 5

If you want to handle quoted numbers with System.Text.Json before .NET 5, then you have to write a custom JSON converter that converts the strings to the target number type.

Here’s an example of a quoted int converter:

using System.Text.Json;
using System.Text.Json.Serialization;

public class QuotedIntConverter : JsonConverter<Int32>
{
	public override Int32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		if (reader.TokenType == JsonTokenType.String)
		{
			return Convert.ToInt32(reader.GetString());
		}
		else
		{
			return reader.GetInt32();
		}
	}

	//Write() not shown
}
Code language: C# (cs)

You’d use it like this:

using System.Text.Json;

var jsonOptions = new JsonSerializerOptions();
jsonOptions.Converters.Add(new QuotedIntConverter());

var movie = JsonSerializer.Deserialize<Movie>(movieJson, jsonOptions);

Console.WriteLine($"Year={movie.YearReleased} Score={movie.Score}");
Code language: C# (cs)

This outputs the following:

Year=2021 Score=8.4

Quoted number handling in ASP.NET Core

By default, ASP.NET Core uses System.Text.Json and uses JsonSerializerDefaults.Web. You can change the JSON settings if you want. In .NET 5+, they added relaxed number handling to the default options:

NumberHandling = JsonNumberHandling.AllowReadingFromString
Code language: C# (cs)

Before .NET 5, this setting was not available so it used strict number handling. This is why if sent requests with quoted numbers, you’d get problem details error responses (400 – Bad Request) like this:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "0HMDORBGUH4Q2:00000001",
    "errors": {
        "$.yearReleased": [
            "The JSON value could not be converted to System.Int32. Path: $.yearReleased | LineNumber: 2 | BytePositionInLine: 22."
        ]
    }
}
Code language: JSON / JSON with Comments (json)

Note: Besides changing the System.Text.Json settings in ASP.NET Core, you can also switch to using Newtonsoft if needed.

Writing quoted numbers during serialization with System.Text.Json

You can use the JsonNumberHandling.WriteAsString option to make it write number properties as strings (quoted numbers) during serialization (in .NET 5 and above):

var movie = new Movie()
{
	Title = "Dune",
	YearReleased = 2021,
	Score = 8.4m
};

var jsonOptions = new JsonSerializerOptions()
{
	NumberHandling = JsonNumberHandling.WriteAsString,
	WriteIndented = true
};

var movieJson = JsonSerializer.Serialize(movie, jsonOptions);

Console.WriteLine(movieJson);
Code language: C# (cs)

This outputs the following:

{
  "Title": "Dune",
  "YearReleased": "2021",
  "Score": "8.4"
}
Code language: JSON / JSON with Comments (json)

Using multiple NumberHandling options

NumberHandling is an enum flag, which means you can set multiple options by bitwise ORing them together.

For example, let’s say you are using the same JsonSerializerOptions object for serialization and deserialization:

var jsonOptions = new JsonSerializerOptions()
{
	NumberHandling = JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString
};
Code language: C# (cs)

And if you’re applying the JsonNumberHandling attribute to some property, you’ll need to set all the number handling options you want to use:

[JsonNumberHandling(JsonNumberHandling.WriteAsString | JsonNumberHandling.AllowReadingFromString)]
public decimal Score { get; set; }
Code language: C# (cs)

Leave a Comment