问题
I am trying to parse a decimal value from json (eg. id: 4.5
) to a poco int
and I want an exception.
Background:
This deserialization throws Newtonsoft.Json.JsonSerializationException
when encountering a decimal where expecting an int
:
httpContent.ReadAsAsync<MyCollection<T>>(
mediaTypeFormatters,
cancellationToken);
MyCollection<T>
is a class with a list Result
of type T
, and T
can have an int
. Now, I want to catch the ones that throw and keep the rest. So I first extract it as a collection of JObject
instead, and then parse them one by one in a try-catch.
var jObjectsMyCollection = await httpContent.ReadAsAsync<MyCollection<Newtonsoft.Json.Linq.JObject>>(
mediaTypeFormatters,
cancellationToken);
foreach (var j in jObjectsMyCollection.Results) {
try {
// now let's parse j one by one
The problem is
I can not make it throw this way, even using the same formatter:
This just deserializes the 4.5
to 4
and does not throw:
var jsonSerializer = JsonSerializer.Create(myMediaTypeFormatters.JsonFormatter.SerializerSettings);
j.ToObject<T>(jsonSerializer)
Same with this:
var ser = myMediaTypeFormatters.JsonFormatter.CreateJsonSerializer();
tObjects.Add(ser.Deserialize<T>(j.CreateReader()));
For the record, the two different formatters used in the different are set up like this:
myMediaTypeFormatters= new MediaTypeFormatterCollection();
myMediaTypeFormatters.JsonFormatter.SerializerSettings.Error += SerializationErrorHander;
myMediaTypeFormatters.JsonFormatter.SerializerSettings.ContractResolver = new SnakeCasePropertyNamesContractResolver();
myMediaTypeFormatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
IEnumerable<MediaTypeFormatter> mediaTypeFormatters = myMediaTypeFormatters;
The Question:
How can I make it throw at exactly the same data as ReadAsAsync
does?
Am I doing soemthing wrong in reusing the MediaTypeFormatters?
回答1:
Json.NET does seem to have inconsistencies in how it converts floating-point JSON values to integers. E.g., using 10.0.2:
JsonConvert.DeserializeObject<int>("4.5")
fails.JToken.Parse("4.5").ToObject<int>()
succeeds and returns 4.JsonConvert.DeserializeObject<uint>("4.5")
succeeds and returns 4.JsonConvert.DeserializeObject<long>("4.5")
succeeds and returns 4.
(In fact, directly deserializing "4.5"
to an int
seems to be the only case that fails. Json.NET will deserialize "4.5"
directly or indirectly to any other integral type.)
The difference seems to arise from inconsistencies between JsonTextReader.ParseReadNumber() (which is called when deserializing JSON directly) and JsonReader.ReadAsInt32() (which is called when deserializing from a JToken
). The former checks that the textual JSON value is really an integer when deserializing to an int
while the latter simply calls Convert.ToInt32() which happily returns a rounded value.
If you want, you could report an issue about the inconsistency.
In the meantime, you have a couple of options to avoid the inconsistency. Firstly, you could introduce a custom JsonConverter for integers that throws an exception when trying to deserialize a floating point value to an integer, then use that when deserializing from a JToken
hierarchy:
public class StrictIntConverter : StrictIntConverterBase
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(int) || objectType == typeof(int?);
}
}
public abstract class StrictIntConverterBase : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Type type = (Nullable.GetUnderlyingType(objectType) ?? objectType);
bool isNullable = (Nullable.GetUnderlyingType(objectType) != null);
if (reader.TokenType == JsonToken.Null)
{
if (isNullable)
return null;
throw new JsonSerializationException(string.Format("Null value for {0}", objectType));
}
else if (reader.TokenType == JsonToken.Float)
{
throw new JsonSerializationException(string.Format("Floating-point value {0} found for {1}.", reader.Value, type));
}
else
{
// Integer or string containing an integer
if (reader.Value.GetType() == type)
return reader.Value;
return JToken.Load(reader).ToObject(type);
}
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
And then do:
var jsonSerializer = JsonSerializer.Create(myMediaTypeFormatters.JsonFormatter.SerializerSettings;
jsonSerializer.Converters.Add(new StrictIntConverter());
j.ToObject<T>(jsonSerializer)
Sample fiddle #1.
Another option would be to use Json.NET's serialization error handling to catch and swallow exceptions when deserializing collection items inside your MyCollection<T>
type, for instance:
public class MyCollection<T> : Collection<T>
{
[OnError]
void OnError(StreamingContext context, ErrorContext errorContext)
{
if (errorContext.OriginalObject != this)
{
// Error occurred deserializing an item in the collection. Swallow it.
errorContext.Handled = true;
}
}
}
This allows you to deserialize directly to your MyCollection<T>
and skip the intermediate JToken
representation. Sample fiddle #2.
来源:https://stackoverflow.com/questions/45772447/how-to-let-deserialization-throw-exception-on-non-integer-where-expecting-an-int