17

When I deseiralize the JSON to the C# object below, either using Newtonsoft explicitly or via the model binding mechanism of ASP.NET Web Api, the string id value is automatically converted to int. I would expect it to throw an exception or raise an error as there is a type mismatch. Is this how JSON is supposed to work in the specs? If not, how can I prevent such an automatic conversion?

JSON: {"id":"4", "name":"a"} C# model: int id; string name

2
  • 1
    That's the feature offered by JSON serialization frameworks such as NewtonSoft and others. In JSON object everyting is string. It's the framework which takes care of type conversion. It will surely throw an error if the json string has "id":"somestring". Framework will try to do type conversion as long as the JSON string value is in format of the target type. Commented Jan 21, 2017 at 19:07
  • 11
    "In JSON object everyting is string"... huh? No. {"a":4} and {"a":"4"} are different. "a" is a JSON number in one and a string in the other. In a JSON object, everything is not a string. Properties can be strings, numbers, other others, or arrays. Interpretting a string as a number (if it can be parsed as a number, and the deserialized type is a number, isn't really a problem, IMO. JSON types are very basic, so if you have a JSON array, it's fine to deserialize it as a List, for example, if that's the target type. Same goes for automatic string to int conversion (and vice versa). Commented Dec 20, 2017 at 22:07

3 Answers 3

33

This is a feature of Json.NET: when deserializing a primitive type, it will convert the primitive JSON value to the target c# type whenever possible. Since the string "4" can be converted to an integer, deserialization succeeds. If you don't want this feature, you can create a custom JsonConverter for integral types that checks that the token being read is really numeric (or null, for a nullable value):

public class StrictIntConverter : JsonConverter
{
    readonly JsonSerializer defaultSerializer = new JsonSerializer();

    public override bool CanConvert(Type objectType) 
    {
        return objectType.IsIntegerType();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        switch (reader.TokenType)
        {
            case JsonToken.Integer:
            case JsonToken.Float: // Accepts numbers like 4.00
            case JsonToken.Null:
                return defaultSerializer.Deserialize(reader, objectType);
            default:
                throw new JsonSerializationException(string.Format("Token \"{0}\" of type {1} was not a JSON integer", reader.Value, reader.TokenType));
        }
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

public static class JsonExtensions
{
    public static bool IsIntegerType(this Type type)
    {
        type = Nullable.GetUnderlyingType(type) ?? type;
        if (type == typeof(long)
            || type == typeof(ulong)
            || type == typeof(int)
            || type == typeof(uint)
            || type == typeof(short)
            || type == typeof(ushort)
            || type == typeof(byte)
            || type == typeof(sbyte)
            || type == typeof(System.Numerics.BigInteger))
            return true;
        return false;
    }        
}

Note the converter accepts values like 4.00 as integers. You can change this by removing the check for JsonToken.Float if it does not meet your needs.

You can apply it to your model directly as follows:

public class RootObject
{
    [JsonConverter(typeof(StrictIntConverter))]
    public int id { get; set; }

    public string name { get; set; }
}

Or include the converter in JsonSerializerSettings to apply it to all integral fields:

var settings = new JsonSerializerSettings
{
    Converters = { new StrictIntConverter() },
};
var root = JsonConvert.DeserializeObject<RootObject>(json, settings);

Finally, to apply JSON serializer settings globally in Web API, see for instance here.

Sign up to request clarification or add additional context in comments.

4 Comments

awesome answer, and should the accepted one. this is the only way I can see to customise peculiar anomaly cases that may occur in certain circumstances.
How to pass return JsonSerializationException to Controller ?
@KonsultanITBandung - The format for questions on stack overflow is one question per post, so you should ask another question.
@dbc I'm so sorry, I'll never do it again.
0

What you describe is a feature, since most folks want this kind of behavior. I haven't check, but I bet it uses something like Convert.ChangeType(strValue, propertyType); that tries to automatically convert from string to target's property type.

If you need it just as string, use Maksim's solution.

Your model can also incorporate an extra property to have both types, if needed:

public class Model
{
    public int id { get; set; }
    public string idStr => id.ToString();

    public string name { get; set; }
}

1 Comment

Do you want to detect ids that do not have the expected type? If yes, you can deserialize as dynamic (check Maksim's solution) and check the type: JsonConvert.DeserializeObject(jsonStr).id.Type
0

This helped me when looking at a similar problem with System.Text.Json. When calling deserialize you can specify disallowing conversion from int to string using JsonSerializerOptions e.g

var options = new JsonSerializerOptions();
options.NumberHandling = 0; //disallow reading from string     
_serializerOptions = options; 
var payload = JsonSerializer.Deserialize<IPayload>(body,_serializerOptions);

3 Comments

The question is specifically about Json.NET, but your answer applies to the new System.Text.Json serializer. I would suggest you edit your answer to clarify your "similar problem" was with System.Text.Json. Thanks!
Also, unlike Json.NET, the default value for JsonSerializerOptions.NumberHandling is already JsonNumberHandling.Strict, so there is no need to set it -- unless you are using the Web defaults.
Only if you are using the Web defaults would you need to reset NumberHandling back to JsonNumberHandling.Strict.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.