using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
public struct RangeOrValue
public int Value { get { return value; } }
public int Min { get { return min; } }
public int Max { get { return max; } }
public bool IsRange { get { return isRange; } }
public RangeOrValue(int min, int max)
public RangeOrValue(int value)
public class RangeOrValueConverter : JsonConverter
const string MinName = "Min";
const string MaxName = "Max";
public override bool CanConvert(Type objectType)
return objectType == typeof(RangeOrValue) || Nullable.GetUnderlyingType(objectType) == typeof(RangeOrValue);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
var range = (RangeOrValue)value;
writer.WriteStartObject();
writer.WritePropertyName(MinName);
writer.WriteValue(range.Min);
writer.WritePropertyName(MaxName);
writer.WriteValue(range.Max);
writer.WriteValue(range.Value);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
switch (reader.MoveToContent().TokenType)
return new RangeOrValue(reader.ValueAsInt32());
case JsonToken.StartObject:
switch (reader.ReadToContentAndAssert().TokenType)
case JsonToken.PropertyName:
var name = (string)reader.Value;
if (name.Equals(MinName, StringComparison.OrdinalIgnoreCase))
min = reader.ReadAsInt32();
else if (name.Equals(MaxName, StringComparison.OrdinalIgnoreCase))
max = reader.ReadAsInt32();
reader.ReadToContentAndAssert().Skip();
case JsonToken.EndObject:
throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
if (max != null && min != null)
return new RangeOrValue(min.Value, max.Value);
throw new JsonSerializationException(string.Format("Missing min or max at path {0}", reader.Path));
throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
public class RangeOrValueConverter : JsonConverter
public override bool CanConvert(Type objectType)
return objectType == typeof(RangeOrValue) || Nullable.GetUnderlyingType(objectType) == typeof(RangeOrValue);
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
var range = (RangeOrValue)value;
var dto = new RangeDTO { Min = range.Min, Max = range.Max };
serializer.Serialize(writer, dto);
writer.WriteValue(range.Value);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
switch (reader.MoveToContent().TokenType)
return new RangeOrValue(reader.ValueAsInt32());
var dto = serializer.Deserialize<RangeDTO>(reader);
return new RangeOrValue(dto.Min, dto.Max);
public static partial class JsonExtensions
public static int ValueAsInt32(this JsonReader reader)
throw new ArgumentNullException();
if (reader.TokenType != JsonToken.Integer)
throw new JsonSerializationException("Value is not Int32");
return Convert.ToInt32(reader.Value, NumberFormatInfo.InvariantInfo);
throw new JsonSerializationException(string.Format("Invalid integer value {0}", reader.Value), ex);
public static JsonReader ReadToContentAndAssert(this JsonReader reader)
throw new ArgumentNullException();
if (reader.TokenType != JsonToken.Comment)
throw new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
public static JsonReader MoveToContent(this JsonReader reader)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None)
while (reader.TokenType == JsonToken.Comment && reader.Read())
public class RangeOrValueConverterTest
public string Property1 { get; set; }
public RangeOrValue? Value { get; set; }
public string Property2 { get; set; }
public RangeOrValue[] Values { get; set; }
public string Property3 { get; set; }
public void Serialization_Value()
Test(new V1.RangeOrValueConverter());
Test(new V2.RangeOrValueConverter());
Console.WriteLine("Done testing.");
static void Test(JsonConverter converter)
Console.WriteLine("\nTesting converter {0}:", converter.GetType().FullName);
Value = new RangeOrValue(10),
Values = new[] { new RangeOrValue(30), new RangeOrValue(40), new RangeOrValue(50), new RangeOrValue(121), new RangeOrValue(121, 122), new RangeOrValue(-1) },
string json = JsonConvert.SerializeObject(model, Formatting.Indented, converter);
var deserializedModel = JsonConvert.DeserializeObject<Model>(json, converter);
var json2 = JsonConvert.SerializeObject(model, Formatting.Indented, converter);
AssertEqual(model, deserializedModel);
TestTruncationThrows(json, converter);
var commentJson = json.Replace("\"Value\":", "\"Value\": /* Comment */").Replace("121,", "121, /* Comment */");
var deserializedModel2 = JsonConvert.DeserializeObject<Model>(json, converter);
AssertEqual(model, deserializedModel2);
Console.WriteLine("Round-tripped {0}", deserializedModel);
Console.WriteLine(json2);
static void TestTruncationThrows(string json, JsonConverter converter)
for (int i = 1; i < json.Length; i++)
Assert.Throws(Is.InstanceOf<JsonException>(),
() => { var bad = json.Substring(0, i); JsonConvert.DeserializeObject<Model>(bad, converter); Console.WriteLine(bad); });
Assert.Throws(Is.InstanceOf<JsonException>(),
() => { var bad = json.Substring(json.Length - i, i); JsonConvert.DeserializeObject<Model>(bad, converter); Console.WriteLine(bad); });
static void AssertEqual(Model model1, Model model2)
Assert.IsTrue(model1.Value.Equals(model2.Value));
Assert.IsTrue(model1.Values.SequenceEqual(model2.Values));
Assert.IsTrue(JToken.DeepEquals(JToken.FromObject(model1), JToken.FromObject(model2)));
public static void Main()
Console.WriteLine("Environment version: " + Environment.Version);
Console.WriteLine("Json.NET version: " + typeof(JsonSerializer).Assembly.FullName);
new RangeOrValueConverterTest().Serialization_Value();
Console.WriteLine("Failed with unhandled exception: ");