diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs index 25acb198..09587b81 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs @@ -3558,6 +3558,13 @@ keyword such as type of business."" Assert.AreEqual(TimeSpan.FromHours(6), deserializeObject.Offset.Offset); Assert.AreEqual(new DateTime(2000, 1, 1), deserializeObject.Offset.Date); } + + [Test] + public void DeserializePropertyNullableDateTimeOffsetExact() + { + NullableDateTimeTestClass d = JsonConvert.DeserializeObject("{\"DateTimeOffsetField\":\"\\/Date(946663200000+0600)\\/\"}"); + Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1), TimeSpan.FromHours(6)), d.DateTimeOffsetField); + } #endif public abstract class LogEvent @@ -3582,7 +3589,7 @@ keyword such as type of business."" } #if !(NET35 || NET20 || SILVERLIGHT) - [Test] + [Test] public void SerializeExpandoObject() { dynamic expando = new ExpandoObject(); @@ -3620,5 +3627,43 @@ keyword such as type of business."" Assert.AreEqual(expando.Complex.DateTime, (DateTime)o["DateTime"]); } #endif + + [Test] + public void DeserializeDecimalExact() + { + decimal d = JsonConvert.DeserializeObject("123456789876543.21"); + Assert.AreEqual(123456789876543.21m, d); + } + + [Test] + public void DeserializeNullableDecimalExact() + { + decimal? d = JsonConvert.DeserializeObject("123456789876543.21"); + Assert.AreEqual(123456789876543.21m, d); + } + + [Test] + public void DeserializeDecimalPropertyExact() + { + string json = "{Amount:123456789876543.21}"; + Invoice i = JsonConvert.DeserializeObject(json); + Assert.AreEqual(123456789876543.21m, i.Amount); + } + + [Test] + public void DeserializeDecimalArrayExact() + { + string json = "[123456789876543.21]"; + IList a = JsonConvert.DeserializeObject>(json); + Assert.AreEqual(123456789876543.21m, a[0]); + } + + [Test] + public void DeserializeDecimalDictionaryExact() + { + string json = "{'Value':123456789876543.21}"; + IDictionary d = JsonConvert.DeserializeObject>(json); + Assert.AreEqual(123456789876543.21m, d["Value"]); + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Bson/BsonReader.cs b/Src/Newtonsoft.Json/Bson/BsonReader.cs index 8db767ad..deca6e7e 100644 --- a/Src/Newtonsoft.Json/Bson/BsonReader.cs +++ b/Src/Newtonsoft.Json/Bson/BsonReader.cs @@ -144,26 +144,54 @@ namespace Newtonsoft.Json.Bson public override byte[] ReadAsBytes() { Read(); - if (TokenType != JsonToken.Bytes) - throw new JsonReaderException("Error reading bytes. Expected bytes but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); - return (byte[])Value; + if (TokenType == JsonToken.Null) + return null; + if (TokenType == JsonToken.Bytes) + return (byte[])Value; + + throw new JsonReaderException("Error reading bytes. Expected bytes but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); + } + + /// + /// Reads the next JSON token from the stream as a . + /// + /// A . + public override decimal? ReadAsDecimal() + { + Read(); + + if (TokenType == JsonToken.Null) + return null; + if (TokenType == JsonToken.Integer || TokenType == JsonToken.Float) + { + SetToken(JsonToken.Float, Convert.ToDecimal(Value)); + return (decimal)Value; + } + + throw new JsonReaderException("Error reading decimal. Expected a number but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); } #if !NET20 /// - /// Reads the next JSON token from the stream as a . + /// Reads the next JSON token from the stream as a . /// /// - /// A . + /// A . /// - public override DateTimeOffset ReadAsDateTimeOffset() + public override DateTimeOffset? ReadAsDateTimeOffset() { Read(); - if (TokenType != JsonToken.Date) - throw new JsonReaderException("Error reading date. Expected bytes but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); - return new DateTimeOffset((DateTime)Value); + if (TokenType == JsonToken.Null) + return null; + if (TokenType == JsonToken.Date) + { + SetToken(JsonToken.Date, new DateTimeOffset((DateTime)Value)); + return (DateTimeOffset)Value; + } + + throw new JsonReaderException("Error reading date. Expected bytes but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); } #endif diff --git a/Src/Newtonsoft.Json/JsonReader.cs b/Src/Newtonsoft.Json/JsonReader.cs index 2ec28834..4f375624 100644 --- a/Src/Newtonsoft.Json/JsonReader.cs +++ b/Src/Newtonsoft.Json/JsonReader.cs @@ -210,12 +210,18 @@ namespace Newtonsoft.Json /// A or a null reference if the next JSON token is null. public abstract byte[] ReadAsBytes(); + /// + /// Reads the next JSON token from the stream as a . + /// + /// A . + public abstract decimal? ReadAsDecimal(); + #if !NET20 /// - /// Reads the next JSON token from the stream as a . + /// Reads the next JSON token from the stream as a . /// - /// A . - public abstract DateTimeOffset ReadAsDateTimeOffset(); + /// A . + public abstract DateTimeOffset? ReadAsDateTimeOffset(); #endif /// diff --git a/Src/Newtonsoft.Json/JsonTextReader.cs b/Src/Newtonsoft.Json/JsonTextReader.cs index 7b45688f..7f13a326 100644 --- a/Src/Newtonsoft.Json/JsonTextReader.cs +++ b/Src/Newtonsoft.Json/JsonTextReader.cs @@ -42,6 +42,7 @@ namespace Newtonsoft.Json { Read, ReadAsBytes, + ReadAsDecimal, #if !NET20 ReadAsDateTimeOffset #endif @@ -332,17 +333,37 @@ namespace Newtonsoft.Json throw CreateJsonReaderException("Unexpected token when reading bytes: {0}. Line {1}, position {2}.", TokenType, _currentLineNumber, _currentLinePosition); } + /// + /// Reads the next JSON token from the stream as a . + /// + /// A . + public override decimal? ReadAsDecimal() + { + _readType = ReadType.ReadAsDecimal; + if (!ReadInternal()) + throw CreateJsonReaderException("Unexpected end when reading decimal: Line {0}, position {1}.", _currentLineNumber, _currentLinePosition); + + if (TokenType == JsonToken.Null) + return null; + if (TokenType == JsonToken.Float) + return (decimal?)Value; + + throw CreateJsonReaderException("Unexpected token when reading decimal: {0}. Line {1}, position {2}.", TokenType, _currentLineNumber, _currentLinePosition); + } + #if !NET20 /// - /// Reads the next JSON token from the stream as a . + /// Reads the next JSON token from the stream as a . /// /// A . - public override DateTimeOffset ReadAsDateTimeOffset() + public override DateTimeOffset? ReadAsDateTimeOffset() { _readType = ReadType.ReadAsDateTimeOffset; if (!ReadInternal()) throw CreateJsonReaderException("Unexpected end when reading date: Line {0}, position {1}.", _currentLineNumber, _currentLinePosition); + if (TokenType == JsonToken.Null) + return null; if (TokenType == JsonToken.Date) return (DateTimeOffset)Value; @@ -708,30 +729,53 @@ namespace Newtonsoft.Json object numberValue; JsonToken numberType; - if (firstChar == '0' && !number.StartsWith("0.", StringComparison.OrdinalIgnoreCase)) + bool nonBase10 = (firstChar == '0' && !number.StartsWith("0.", StringComparison.OrdinalIgnoreCase)); + + if (_readType == ReadType.ReadAsDecimal) { - numberValue = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? Convert.ToInt64(number, 16) - : Convert.ToInt64(number, 8); - numberType = JsonToken.Integer; - } - else if (number.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || number.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) - { - numberValue = Convert.ToDouble(number, CultureInfo.InvariantCulture); + if (nonBase10) + { + // decimal.Parse doesn't support parsing hexadecimal values + long integer = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? Convert.ToInt64(number, 16) + : Convert.ToInt64(number, 8); + + numberValue = Convert.ToDecimal(integer); + } + else + { + numberValue = decimal.Parse(number, NumberStyles.Number | NumberStyles.AllowExponent); + } + numberType = JsonToken.Float; } else { - try + if (nonBase10) { - numberValue = Convert.ToInt64(number, CultureInfo.InvariantCulture); + numberValue = number.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? Convert.ToInt64(number, 16) + : Convert.ToInt64(number, 8); + numberType = JsonToken.Integer; } - catch (OverflowException ex) + else if (number.IndexOf(".", StringComparison.OrdinalIgnoreCase) != -1 || number.IndexOf("e", StringComparison.OrdinalIgnoreCase) != -1) { - throw new JsonReaderException("JSON integer {0} is too large or small for an Int64.".FormatWith(CultureInfo.InvariantCulture, number), ex); + numberValue = Convert.ToDouble(number, CultureInfo.InvariantCulture); + numberType = JsonToken.Float; } + else + { + try + { + numberValue = Convert.ToInt64(number, CultureInfo.InvariantCulture); + } + catch (OverflowException ex) + { + throw new JsonReaderException("JSON integer {0} is too large or small for an Int64.".FormatWith(CultureInfo.InvariantCulture, number), ex); + } - numberType = JsonToken.Integer; + numberType = JsonToken.Integer; + } } _buffer.Position = 0; diff --git a/Src/Newtonsoft.Json/JsonValidatingReader.cs b/Src/Newtonsoft.Json/JsonValidatingReader.cs index b90abc6c..fd351c96 100644 --- a/Src/Newtonsoft.Json/JsonValidatingReader.cs +++ b/Src/Newtonsoft.Json/JsonValidatingReader.cs @@ -318,14 +318,26 @@ namespace Newtonsoft.Json return data; } + /// + /// Reads the next JSON token from the stream as a . + /// + /// A . + public override decimal? ReadAsDecimal() + { + decimal? d = _reader.ReadAsDecimal(); + + ValidateCurrentToken(); + return d; + } + #if !NET20 /// - /// Reads the next JSON token from the stream as a . + /// Reads the next JSON token from the stream as a . /// - /// A . - public override DateTimeOffset ReadAsDateTimeOffset() + /// A . + public override DateTimeOffset? ReadAsDateTimeOffset() { - DateTimeOffset dateTimeOffset = _reader.ReadAsDateTimeOffset(); + DateTimeOffset? dateTimeOffset = _reader.ReadAsDateTimeOffset(); ValidateCurrentToken(); return dateTimeOffset; diff --git a/Src/Newtonsoft.Json/Linq/JTokenReader.cs b/Src/Newtonsoft.Json/Linq/JTokenReader.cs index 7bd68fec..1e7bf8e1 100644 --- a/Src/Newtonsoft.Json/Linq/JTokenReader.cs +++ b/Src/Newtonsoft.Json/Linq/JTokenReader.cs @@ -46,25 +46,49 @@ namespace Newtonsoft.Json.Linq SetToken(JsonToken.Bytes, data); } - if (TokenType == JsonToken.Bytes) - return (byte[])Value; if (TokenType == JsonToken.Null) return null; + if (TokenType == JsonToken.Bytes) + return (byte[])Value; throw new JsonReaderException("Error reading bytes. Expected bytes but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); } -#if !NET20 /// - /// Reads the next JSON token from the stream as a . + /// Reads the next JSON token from the stream as a . /// - /// A . - public override DateTimeOffset ReadAsDateTimeOffset() + /// A . + public override decimal? ReadAsDecimal() { Read(); + if (TokenType == JsonToken.Null) + return null; + if (TokenType == JsonToken.Integer || TokenType == JsonToken.Float) + { + SetToken(JsonToken.Float, Convert.ToDecimal(Value)); + return (decimal) Value; + } + + throw new JsonReaderException("Error reading decimal. Expected a number but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); + } + +#if !NET20 + /// + /// Reads the next JSON token from the stream as a . + /// + /// A . + public override DateTimeOffset? ReadAsDateTimeOffset() + { + Read(); + + if (TokenType == JsonToken.Null) + return null; if (TokenType == JsonToken.Date) - return new DateTimeOffset((DateTime)Value); + { + SetToken(JsonToken.Date, new DateTimeOffset((DateTime)Value)); + return (DateTimeOffset)Value; + } throw new JsonReaderException("Error reading date. Expected bytes but got {0}.".FormatWith(CultureInfo.InvariantCulture, TokenType)); } diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs index 2ab32955..2c60ac4a 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs @@ -114,7 +114,7 @@ namespace Newtonsoft.Json.Serialization if (reader == null) throw new ArgumentNullException("reader"); - if (reader.TokenType == JsonToken.None && !reader.Read()) + if (reader.TokenType == JsonToken.None && !ReadForType(reader, objectType, null)) return null; return CreateValueNonProperty(reader, objectType, GetContractSafe(objectType)); @@ -607,7 +607,8 @@ namespace Newtonsoft.Json.Serialization throw new JsonSerializationException("Could not convert string '{0}' to dictionary key type '{1}'. Create a TypeConverter to convert from the string to the key type object.".FormatWith(CultureInfo.InvariantCulture, reader.Value, contract.DictionaryKeyType), ex); } - CheckedRead(reader); + if (!ReadForType(reader, contract.DictionaryValueType, null)) + throw new JsonSerializationException("Unexpected end when deserializing object."); try { @@ -651,6 +652,23 @@ namespace Newtonsoft.Json.Serialization }); } + private bool ReadForTypeArrayHack(JsonReader reader, Type t) + { + // this is a nasty hack because calling ReadAsDecimal for example will error when we hit the end of the array + // need to think of a better way of doing this + try + { + return ReadForType(reader, t, null); + } + catch (JsonReaderException) + { + if (reader.TokenType == JsonToken.EndArray) + return true; + + throw; + } + } + private object PopulateList(IWrappedCollection wrappedList, JsonReader reader, string reference, JsonArrayContract contract) { object list = wrappedList.UnderlyingCollection; @@ -662,7 +680,7 @@ namespace Newtonsoft.Json.Serialization int initialDepth = reader.Depth; - while (reader.Read()) + while (ReadForTypeArrayHack(reader, contract.CollectionItemType)) { switch (reader.TokenType) { @@ -896,6 +914,36 @@ namespace Newtonsoft.Json.Serialization return createdObject; } + private bool ReadForType(JsonReader reader, Type t, JsonConverter propertyConverter) + { + // don't read properties with converters as a specific value + // the value might be a string which will then get converted which will error if read as date for example + bool hasConverter = (GetConverter(GetContractSafe(t), propertyConverter) != null); + + if (hasConverter) + return reader.Read(); + + if (t == typeof(byte[])) + { + reader.ReadAsBytes(); + return true; + } + else if ((t == typeof(decimal) || t == typeof(decimal?))) + { + reader.ReadAsDecimal(); + return true; + } +#if !NET20 + else if ((t == typeof(DateTimeOffset) || t == typeof(DateTimeOffset?))) + { + reader.ReadAsDateTimeOffset(); + return true; + } +#endif + + return reader.Read(); + } + private object PopulateObject(object newObject, JsonReader reader, JsonObjectContract contract, string id) { contract.InvokeOnDeserializing(newObject, Serializer.Context); @@ -928,25 +976,8 @@ namespace Newtonsoft.Json.Serialization continue; } - // don't read properties with converters as a specific value - // the value might be a string which will then get converted which will error if read as date for example - bool hasConverter = (GetConverter(GetContractSafe(property.PropertyType), property.Converter) != null); - - if (property.PropertyType == typeof(byte[]) && !hasConverter) - { - reader.ReadAsBytes(); - } -#if !NET20 - else if (property.PropertyType == typeof(DateTimeOffset) && !hasConverter) - { - reader.ReadAsDateTimeOffset(); - } -#endif - else - { - if (!reader.Read()) - throw new JsonSerializationException("Unexpected end when setting {0}'s value.".FormatWith(CultureInfo.InvariantCulture, memberName)); - } + if (!ReadForType(reader, property.PropertyType, property.Converter)) + throw new JsonSerializationException("Unexpected end when setting {0}'s value.".FormatWith(CultureInfo.InvariantCulture, memberName)); SetRequiredProperty(reader, property, requiredProperties);