16

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

John L.
  • 1,825
  • 5
  • 18
  • 45
  • 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. – Chetan Jan 21 '17 at 19:07
  • 10
    "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). – Triynko Dec 20 '17 at 22:07

2 Answers2

30

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
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; }
}
Alexei - check Codidact
  • 22,016
  • 16
  • 145
  • 164
  • Do you want to detect `id`s 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` – Alexei - check Codidact Jan 21 '17 at 19:46