Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid deserializing a JSON string token to a numeric property

The JSON looks like this:

{
    "x": "50"
}

And the class looks like this:

public class Test
{
    public float? x { get; set; }
}

And when using

var test = JsonConvert.DeserializeObject<Test>(json);

No exception is thrown, JSON.NET just converts the string value "50" into a float value 50.0.

This question is raised in the the context of input-validation. I want to get an exception, because the JSON string does not comply to the contract (the x field should be a real float).

And I don't want to use property annotations in the 'Test' class.

Is there a JsonSerializerSettings which can be be used to avoid this?

like image 541
Stef Heyenrath Avatar asked Mar 08 '16 13:03

Stef Heyenrath


1 Answers

JSON.NET liberally parses numbers-in-strings ("50") as numbers. There's no trivial way to turn this off, as far as I can find.

You could create a custom converter that disallows this:

public class NumberConverter : JsonConverter
{
    private readonly Type[] _typesNotToReadAsString = { typeof(float), typeof(float?) };

    public override bool CanConvert(Type objectType)
    {
        return _typesNotToReadAsString.Any(t => t.IsAssignableFrom(objectType));
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);

        if (_typesNotToReadAsString.Contains(objectType) && token.Type == JTokenType.String)
        {
            string exceptionString = string.Format("Won't convert string to type {0}", objectType.FullName);
            throw new JsonSerializationException(exceptionString);
        }

        return token.ToObject(objectType);
    }

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

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

The converter reports to be able to deserialize into specified types, in this case float and float?, but configurable.

Upon deserialization, it inspects the token type. Some token types for given JSON input:

  • "50": JTokenType.String
  • 50: JTokenType.Integer
  • 42.1415: JTokenType.Float

This way the converter can determine whether the current token is formatted as desired. When the token type is a string, the above converter will throw an exception stating it won't convert a string to the desired type.

When the token type is anything else, the converter will convert the token to the appropriate numeric type through token.ToObject(objectType). That method will also handle non-numeric input by throwing the appropriate exception, for example "Can not convert Array to Single.".

Given a class Foo:

public class Foo
{
    public float Bar { get; set; }
    public string Baz { get; set; }
    public float? Qux { get; set; }
}

Deserialize a JSON string using above converter, this will work:

var jsonString = "{ \"Bar\" : 50, \"Baz\" : \"zaB\", \"Qux\" : 42.1415 }";
var foo = JsonConvert.DeserializeObject<Foo>(jsonString, new NumberConverter());

While this will throw:

var jsonString = "{ \"Bar\" : 50, \"Baz\" : \"zaB\", \"Qux\" : \"42.1415\" }";
var foo2 = JsonConvert.DeserializeObject<Foo>(jsonString, new NumberConverter());
like image 74
CodeCaster Avatar answered Sep 28 '22 09:09

CodeCaster