Skip to content

Commit 192a093

Browse files
JsonConverter for decimal and decimal? types (#9291)
* Create universal JsonConverter for decimal and decimal? types * use it only for read * removed unused import
1 parent 4d61f42 commit 192a093

2 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Globalization;
18+
using Newtonsoft.Json;
19+
20+
namespace QuantConnect.Util;
21+
22+
/// <summary>
23+
/// Json converter to represent decimals as strings
24+
/// </summary>
25+
public class DecimalJsonConverter : JsonConverter
26+
{
27+
/// <summary>
28+
/// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
29+
/// </summary>
30+
/// <remarks>
31+
/// This property always returns <c>false</c>, indicating that this converter does not support writing JSON.
32+
/// </remarks>
33+
public override bool CanWrite => false;
34+
35+
/// <summary>
36+
/// Determines whether this instance can convert the specified object type.
37+
/// </summary>
38+
/// <param name="objectType">Type of the object.</param>
39+
/// <returns><c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.</returns>
40+
public override bool CanConvert(Type objectType)
41+
{
42+
return objectType == typeof(decimal) || objectType == typeof(decimal?);
43+
}
44+
45+
/// <summary>
46+
/// Writes the JSON representation of the object.
47+
/// </summary>
48+
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
49+
/// <param name="value">The value.</param>
50+
/// <param name="serializer">The calling serializer.</param>
51+
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
52+
{
53+
throw new NotImplementedException();
54+
}
55+
56+
/// <summary>
57+
/// Reads the JSON representation of the object.
58+
/// </summary>
59+
/// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
60+
/// <param name="objectType">Type of the object.</param>
61+
/// <param name="existingValue">The existing value of object being read.</param>
62+
/// <param name="serializer">The calling serializer.</param>
63+
/// <returns>The object value.</returns>
64+
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
65+
{
66+
var val = reader.Value;
67+
68+
if (val == null)
69+
{
70+
if (objectType == typeof(decimal?))
71+
{
72+
return null;
73+
}
74+
throw new JsonSerializationException($"Cannot convert null value to {objectType}.");
75+
}
76+
77+
if (val is decimal dec)
78+
{
79+
return dec;
80+
}
81+
82+
if (val is double d)
83+
{
84+
return d.SafeDecimalCast();
85+
}
86+
87+
if (val is string str && TryParse(str, out var res))
88+
{
89+
return res;
90+
}
91+
92+
return Convert.ToDecimal(val, CultureInfo.InvariantCulture);
93+
}
94+
95+
private static bool TryParse(string str, out decimal value)
96+
{
97+
return decimal.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out value);
98+
}
99+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using Newtonsoft.Json;
19+
using NUnit.Framework;
20+
using QuantConnect.Util;
21+
22+
namespace QuantConnect.Tests.Common.Util;
23+
24+
public class DecimalJsonConverterTests
25+
{
26+
private static readonly JsonSerializerSettings settings = new JsonSerializerSettings
27+
{
28+
Converters = new List<JsonConverter>
29+
{
30+
new DecimalJsonConverter()
31+
}
32+
};
33+
34+
[TestCase("1", 1, true)]
35+
[TestCase("0", 0, true)]
36+
[TestCase("-1", -1, true)]
37+
[TestCase("1.333", 1.333, true)]
38+
[TestCase("1.333", 1.333, false)]
39+
[TestCase("9e-8", 0.00000009, true)]
40+
[TestCase("1.3117285e+06", 1311728.5, true)]
41+
[TestCase("9e-8", 0.00000009, false)]
42+
[TestCase("1.3117285e+06", 1311728.5, false)]
43+
public void DecimalStringConverterTests(string value, decimal expected, bool quote = true)
44+
{
45+
var jsonString = TestObject<decimal>.CreateJsonObject(value, quote);
46+
var obj = JsonConvert.DeserializeObject<TestObject<decimal>>(jsonString, settings);
47+
48+
Assert.AreEqual(expected, obj.Value);
49+
}
50+
51+
[TestCase("1", 1, true)]
52+
[TestCase("0", 0, true)]
53+
[TestCase("-1", -1, true)]
54+
[TestCase("1.333", 1.333, true)]
55+
[TestCase("1.333", 1.333, false)]
56+
[TestCase("9e-8", 0.00000009, true)]
57+
[TestCase("1.3117285e+06", 1311728.5, true)]
58+
[TestCase("9e-8", 0.00000009, false)]
59+
[TestCase("1.3117285e+06", 1311728.5, false)]
60+
public void NullableDecimalStringConverterTests(string value, decimal expected, bool quote = true)
61+
{
62+
var jsonString = TestObject<decimal?>.CreateJsonObject(value, quote);
63+
var obj = JsonConvert.DeserializeObject<TestObject<decimal?>>(jsonString, settings);
64+
65+
Assert.AreEqual(expected, obj.Value);
66+
}
67+
68+
[Test]
69+
public void ThrowsConversionError()
70+
{
71+
var jsonString = """
72+
{"value": "qwerty"}
73+
""";
74+
75+
Assert.Throws<FormatException>(() =>
76+
JsonConvert.DeserializeObject<TestObject<decimal>>(jsonString, settings));
77+
}
78+
79+
[Test]
80+
public void NullableDecimalHandlesNull()
81+
{
82+
var jsonString = """
83+
{"value": null}
84+
""";
85+
var obj = JsonConvert.DeserializeObject<TestObject<decimal?>>(jsonString, settings);
86+
87+
Assert.IsNull(obj.Value);
88+
}
89+
90+
[Test]
91+
public void NonNullableDecimalThrowsOnNull()
92+
{
93+
var jsonString = """
94+
{"value": null}
95+
""";
96+
97+
Assert.Throws<JsonSerializationException>(() =>
98+
JsonConvert.DeserializeObject<TestObject<decimal>>(jsonString, settings));
99+
}
100+
101+
[Test]
102+
public void SerializesDecimalAsString()
103+
{
104+
var obj = new TestObject<decimal> { Value = 1.333m };
105+
var json = JsonConvert.SerializeObject(obj, settings);
106+
var jsonOrigin = JsonConvert.SerializeObject(obj);
107+
108+
Assert.AreEqual(jsonOrigin, json);
109+
}
110+
111+
[Test]
112+
public void SerializesNullableDecimalAsStringFallbackToOriginBehavior()
113+
{
114+
var obj = new TestObject<decimal?> { Value = 1.333m };
115+
var json = JsonConvert.SerializeObject(obj, settings);
116+
var jsonOrigin = JsonConvert.SerializeObject(obj);
117+
118+
Assert.AreEqual(jsonOrigin, json);
119+
}
120+
121+
[Test]
122+
public void SerializesNullableDecimalNullAsNull()
123+
{
124+
var obj = new TestObject<decimal?> { Value = null };
125+
var json = JsonConvert.SerializeObject(obj, settings);
126+
var jsonOrigin = JsonConvert.SerializeObject(obj);
127+
128+
Assert.AreEqual(jsonOrigin, json);
129+
}
130+
131+
private class TestObject<T>
132+
{
133+
public T Value { get; set; }
134+
135+
public static string CreateJsonObject(string value, bool quote = false)
136+
{
137+
if (quote)
138+
{
139+
value = $"\"{value}\"";
140+
}
141+
142+
return $$"""
143+
{"value": {{value}}}
144+
""";
145+
}
146+
}
147+
}

0 commit comments

Comments
 (0)