I'm trying to create a simple JsonConverter. I want my byte arrays to be serialized as an array of numbers instead of the default base 64 string. However, I'm getting an JsonSerializationException when I'm trying to do that.
Here's a class I made to simplify my problem:
public class SomethingFancy
{
string name;
byte[] usefulData;
public SomethingFancy(string name, byte[] usefulData)
{
this.Name = name;
this.UsefulData = usefulData;
}
public string Name { get => name; set => name = value; }
public byte[] UsefulData { get => usefulData; set => usefulData = value; }
}
Now here's is my custom Json Converter. I tried to make it work only with IEnumerable objects. (by default, IEnumerable is converted to a string when serializing and viceversa when deserializing. I changed that behavior to save the IEnumerable as a json array of numbers instead.
public class EnumerableByteConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var result = typeof(IEnumerable<byte>).IsAssignableFrom(objectType);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteValue(value);
}
else
{
byte[] bytes = ((IEnumerable<byte>)value).ToArray();
int[] ints = Array.ConvertAll(bytes, c => (int)c);
writer.WriteStartArray();
foreach (int number in ints)
{
writer.WriteValue(number);
}
writer.WriteEndArray();
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
int[] ints = (int[])reader.Value;
if (ints == null)
return default;
else
{
byte[] bytes = ints.SelectMany(BitConverter.GetBytes).ToArray();
if (objectType == typeof(byte[]))
{
return bytes;
}
var result = new List<byte>(bytes);
return result;
}
}
}
And here's some Unit test I wrote to test my class:
[TestClass]
public class PersistencyServiceTest
{
[TestMethod]
public void TestJsonSerializationDeserialization()
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new EnumerableByteConverter());
SomethingFancy something = new SomethingFancy("someName", new byte[3] { 1, 2, 3 });
string dataasstring = JsonConvert.SerializeObject(something, Formatting.Indented, settings);
something = JsonConvert.DeserializeObject<SomethingFancy>(dataasstring, settings);
Assert.IsTrue(something != null);
Assert.IsTrue(something.Name == "someName");
Assert.IsTrue(something.UsefulData != null);
Assert.IsTrue(something.UsefulData[0] == 1);
Assert.IsTrue(something.UsefulData[1] == 2);
Assert.IsTrue(something.UsefulData[2] == 3);
}
}
Now, it serializes my object just as I needed.
{
"Name": "someName",
"UsefulData": [
1,
2,
3
]
}
However, the deserialization is throwing a JsonSerializationException (Unexpected token when deserializing object: Integer. Path 'UsefulData[0], line 4, position 5).
What am I missing?
Thanks for your help.
I was doing everything wrong with the ReadJson method.
Here's the custom JsonConverter for people with the same problema I had:
public class EnumerableByteConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var result = typeof(IEnumerable<byte>).IsAssignableFrom(objectType);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteValue(value);
}
else
{
byte[] bytes = ((IEnumerable<byte>)value).ToArray();
int[] ints = Array.ConvertAll(bytes, c => (int)c);
writer.WriteStartArray();
foreach (int number in ints)
{
writer.WriteValue(number);
}
writer.WriteEndArray();
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
List<int> ints = null;
if (reader.TokenType == JsonToken.Null)
return default;
while (reader.TokenType != JsonToken.EndArray)
{
if (reader.TokenType == JsonToken.StartArray)
{
ints = new List<int>();
reader.Read();
}
else if(reader.TokenType == JsonToken.Integer)
{
ints.Add(Convert.ToInt32(reader.Value));
reader.Read();
}
else
{
throw new InvalidOperationException();
}
}
if (ints == null)
return default;
else
{
byte[] bytes = Array.ConvertAll(ints.ToArray(), x => (byte)x);
if (objectType == typeof(byte[]))
{
return bytes;
}
var result = new List<byte>(bytes);
return result;
}
}
}
Related
I want to use a custom JsonConverter for string arrays (or IEnumerable) and do some manipulations on the array (actually removing all strings that are null or whitespace).
But I am already stuck in the ReadJson method not knowing how to correctly get the string[].
I did a custom converter for simple strings where I checked for JsonToken.String. But arrays have StartArray and EndArray...
Anyone who already de/serialized their custom string arrays and could help me out?
More details:
What I want to achieve is centralized or optional string trimming on model binding (so I don't have to do that in every controller) and model validation checking for duplicates would detect "a string" and " a string" as duplicate.
I am trying to do that as JsonConverters (digging down model bindign log output, .net core docs, .net core github code brought me to the point a json converter is best).
Centralized usage would be configured in the StartUp Json Options:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions
(
options =>
{
options.SerializerSettings.Converters.Add(new TrimmedStringConverter());
options.SerializerSettings.Converters.Add(new CleanStringArrayConverter());
}
);
}
Usage on a per model basis it would look like
public class RequestModel
{
[JsonConverter(typeof(TrimmedStringConverter))]
public string MyValue { get; set; }
[JsonConverter(typeof(CleanStringArrayConverter))]
public string[] Entries { get; set; }
}
This question provided the converter for automatically trim strings on model binding. I just added some salt.
public class TrimmedStringConverter : JsonConverter
{
public bool EmptyStringsAsNull { get; }
public TrimmedStringConverter()
{
EmptyStringsAsNull = true;
}
public TrimmedStringConverter(bool emptyStringsAsNull)
{
EmptyStringsAsNull = emptyStringsAsNull;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(string);
}
private string CleanString(string str)
{
if (str == null) return null;
str = str.Trim();
if (str.Length == 0 && EmptyStringsAsNull) return null;
return str;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.String)
{
//if (reader.Value != null)
return CleanString(reader.Value as string);
}
return reader.Value;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var text = (string)value;
if (text == null)
writer.WriteNull();
else
writer.WriteValue(CleanString(text));
}
}
Now this leaves my model having empty strings or nulls in the string[]. Which I now try to to automatically remove in a second converter (or a converter doing the same above but for string arrays, collections).
I just can't figure out how to correctly handle the array serialization/deserialization with the reader and serializer.
That's how far I got (a thank you to Silvermind). A second converter for string arrays.
First I managed to use the globally registered TrimmedStringConverter in the CleanStringArrayConverter (check the additional out-commented code) too. This worked as long as the TrimmedStringConverter was used globally and the CleanStringArrayConverter was on a per model basis. Using both globally cause infinite loops and SERVER CRASHES with an Access Violation exception.
So I changed it to this version where both can be registered globally side by side.
Unfortunatly it will only work for arrays.
May once someone of you will find this code, uses it and can share improvements?
public class CleanStringArrayConverter : JsonConverter
{
public bool TrimStrings { get; }
public bool EmptyStringsAsNull { get; }
public bool RemoveWhitespace { get; }
public bool RemoveNulls { get; }
public bool RemoveEmpty { get; }
public CleanStringArrayConverter()
{
TrimStrings = true;
EmptyStringsAsNull = true;
RemoveWhitespace = true;
RemoveNulls = true;
RemoveEmpty = true;
}
public CleanStringArrayConverter(bool trimStrings = true, bool emptyStringsAsNull = true, bool removeWhitespace = true, bool removeEmpty = true, bool removeNulls = true)
{
TrimStrings = trimStrings;
EmptyStringsAsNull = emptyStringsAsNull;
RemoveWhitespace = removeWhitespace;
RemoveNulls = removeNulls;
RemoveEmpty = removeEmpty;
}
private string CleanString(string str)
{
if (str == null) return null;
if (TrimStrings) str = str.Trim();
if (str.Length == 0 && EmptyStringsAsNull) return null;
return str;
}
private string[] CleanStringCollection(IEnumerable<string> strings)
{
if (strings == null) return null;
return strings
.Select(s => CleanString(s))
.Where
(
s =>
{
if (s == null) return !RemoveNulls;
else if (s.Equals(string.Empty)) return !RemoveEmpty;
else if (string.IsNullOrWhiteSpace(s)) return !RemoveWhitespace;
else return true;
}
)
.ToArray();
}
public override bool CanConvert(Type objectType)
{
return objectType.IsArray && objectType.GetElementType() == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string[] arr = null; // use null as default value
//string[] arr = new string[]; // use empty array as default value
// deserialze the array
if (reader.TokenType != JsonToken.Null)
{
if (reader.TokenType == JsonToken.StartArray)
{
// this one respects other registered converters (e.g. the TrimmedStringConverter)
// but causes server crashes when used globally due to endless loops
//arr = serializer.Deserialize<string[]>(reader);
// this doesn't respect others!!!
JToken token = JToken.Load(reader);
arr = token.ToObject<string[]>();
}
}
// clean up the array
if (arr != null) arr = CleanStringCollection(arr);
return arr;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
string[] arr = (string[])value;
if (value == null)
{
writer.WriteNull();
return;
}
arr = CleanStringCollection(arr);
// endless loops and server crashes!!!
//serializer.Serialize(writer, arr);
writer.WriteStartArray();
string v;
foreach(string s in arr)
{
v = CleanString(s);
if (v == null)
writer.WriteNull();
else
writer.WriteValue(v);
}
writer.WriteEndArray();
}
}
It is basically the same idea:
internal sealed class TrimmedStringCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsArray && objectType.GetElementType() == typeof(string);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (existingValue is null)
{
// Returning empty array???
return new string[0];
}
var array = (string[])existingValue;
return array.Where(s => !String.IsNullOrEmpty(s)).ToArray();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value);
}
}
Perhaps you might want to do the same for the write part.
I have this model that I want to serialize and deserialize using Json.Net:
public struct RangeOrValue
{
public int Value { get; }
public int Min { get; }
public int Max { get; }
public bool IsRange { get; }
public RangeOrValue(int min, int max)
{
Min = min;
Max = max;
IsRange = true;
Value = 0;
}
public RangeOrValue(int value)
{
Min = 0;
Max = 0;
Value = value;
IsRange = false;
}
}
I have a special requirement for serialization. If the first constructor is used, then the value should be serialized as { "Min": <min>, "Max": <max> }.
But if the second constructor is used, then value should be serialized as <value>.
For example new RangeOrValue(0, 10) needs to be serialized as { "Min": 0, "Max": 10 } and new RangeOrValue(10) needs to be serialized as 10.
I wrote this custom converter to do this task:
public class RangeOrValueConverter : JsonConverter<RangeOrValue>
{
public override void WriteJson(JsonWriter writer, RangeOrValue value, JsonSerializer serializer)
{
if (value.IsRange)
{
// Range values are stored as objects
writer.WriteStartObject();
writer.WritePropertyName("Min");
writer.WriteValue(value.Min);
writer.WritePropertyName("Max");
writer.WriteValue(value.Max);
writer.WriteEndObject();
}
else
{
writer.WriteValue(value.Value);
}
}
public override RangeOrValue ReadJson(JsonReader reader, Type objectType, RangeOrValue existingValue, bool hasExistingValue, JsonSerializer serializer)
{
reader.Read();
// If the type is range, then first token should be property name ("Min" property)
if (reader.TokenType == JsonToken.PropertyName)
{
// Read min value
int min = reader.ReadAsInt32() ?? 0;
// Read next property name
reader.Read();
// Read max value
int max = reader.ReadAsInt32() ?? 0;
// Read object end
reader.Read();
return new RangeOrValue(min, max);
}
// Read simple int
return new RangeOrValue(Convert.ToInt32(reader.Value));
}
}
To test the functionality, I wrote this simple test:
[TestFixture]
public class RangeOrValueConverterTest
{
public class Model
{
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; }
}
[Test]
public void Serialization_Value()
{
var model = new Model
{
Value = new RangeOrValue(10),
Values = new[] {new RangeOrValue(30), new RangeOrValue(40), new RangeOrValue(50),},
Property1 = "P1",
Property2 = "P2",
Property3 = "P3"
};
string json = JsonConvert.SerializeObject(model, new RangeOrValueConverter());
var deserializedModel = JsonConvert.DeserializeObject<Model>(json, new RangeOrValueConverter());
Assert.AreEqual(model, deserializedModel);
}
}
When I run the test, Object serializes successfully. But when it tries to deserialize it back, I receive this error:
Newtonsoft.Json.JsonReaderException : Could not convert string to integer: P2. Path 'Property2', line 1, position 46.
The stack trace leads to line int min = reader.ReadAsInt32() ?? 0;.
I think I'm doing something wrong in converter that causes Json.Net to provide wrong values to the converter. But I can't quite figure it out. Any ideas?
Your basic problem is that, at the beginning of ReadJson(), you unconditionally call Read() to advance the reader past the current token:
public override RangeOrValue ReadJson(JsonReader reader, Type objectType, RangeOrValue existingValue, bool hasExistingValue, JsonSerializer serializer)
{
reader.Read();
However, if the current token is an integer corresponding to a RangeOrValue with a single value, then you have just skipped past that value, leaving the reader positioned on whatever comes next. Instead you need to process the current value when that value is of type JsonToken.Integer.
That being said, there are several other possible issues with your converter, mostly related to the fact that you assume that the incoming JSON is in a particular format, rather than validating that fact:
According to the JSON standard an object is an unordered set of name/value pairs but ReadJson() assumes a specific property order.
ReadJson() doesn't skip past or error on unknown properties.
ReadJson() doesn't error on truncated files.
ReadJson() doesn't error on unexpected token types (say, an array instead of an object or integer).
If the JSON file contains comments (which are not included in the JSON standard but are supported by Json.NET) then ReadJson() will not handle this.
The converter doesn't handle Nullable<RangeOrValue> members.
Note that if you inherit from JsonConverter<T>, then you will have to write separate converters for T and Nullable<T>. Thus, for structs, I think it's easier to inherit from the base class JsonConverter.
A JsonConverter that handles these issues would look something like the following:
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;
if (range.IsRange)
{
// Range values are stored as objects
writer.WriteStartObject();
writer.WritePropertyName(MinName);
writer.WriteValue(range.Min);
writer.WritePropertyName(MaxName);
writer.WriteValue(range.Max);
writer.WriteEndObject();
}
else
{
writer.WriteValue(range.Value);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.MoveToContent().TokenType)
{
case JsonToken.Null:
// nullable RangeOrValue; return null.
return null;
case JsonToken.Integer:
return new RangeOrValue(reader.ValueAsInt32());
case JsonToken.StartObject:
int? min = null;
int? max = null;
var done = false;
while (!done)
{
// Read the next token skipping comments if any
switch (reader.ReadToContentAndAssert().TokenType)
{
case JsonToken.PropertyName:
var name = (string)reader.Value;
if (name.Equals(MinName, StringComparison.OrdinalIgnoreCase))
// ReadAsInt32() reads the NEXT token as an Int32, thus advancing past the property name.
min = reader.ReadAsInt32();
else if (name.Equals(MaxName, StringComparison.OrdinalIgnoreCase))
max = reader.ReadAsInt32();
else
// Unknown property name. Skip past it and its value.
reader.ReadToContentAndAssert().Skip();
break;
case JsonToken.EndObject:
done = true;
break;
default:
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));
default:
throw new JsonSerializationException(string.Format("Invalid token type {0} at path {1}", reader.TokenType, reader.Path));
}
}
}
Using the extension methods:
public static partial class JsonExtensions
{
public static int ValueAsInt32(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType != JsonToken.Integer)
throw new JsonSerializationException("Value is not Int32");
try
{
return Convert.ToInt32(reader.Value, NumberFormatInfo.InvariantInfo);
}
catch (Exception ex)
{
// Wrap the system exception in a serialization exception.
throw new JsonSerializationException(string.Format("Invalid integer value {0}", reader.Value), ex);
}
}
public static JsonReader ReadToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
while (reader.Read())
{
if (reader.TokenType != JsonToken.Comment)
return reader;
}
throw new JsonReaderException(string.Format("Unexpected end at path {0}", reader.Path));
}
public static JsonReader MoveToContent(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None)
if (!reader.Read())
return reader;
while (reader.TokenType == JsonToken.Comment && reader.Read())
;
return reader;
}
}
However, if you're willing to pay a slight performance penalty, the converter can be simplified by serializing and deserializing a DTO as shown below, which uses the same extension method class:
public class RangeOrValueConverter : JsonConverter
{
class RangeDTO
{
public int Min, 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;
if (range.IsRange)
{
var dto = new RangeDTO { Min = range.Min, Max = range.Max };
serializer.Serialize(writer, dto);
}
else
{
writer.WriteValue(range.Value);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
switch (reader.MoveToContent().TokenType)
{
case JsonToken.Null:
// nullable RangeOrValue; return null.
return null;
case JsonToken.Integer:
return new RangeOrValue(reader.ValueAsInt32());
default:
var dto = serializer.Deserialize<RangeDTO>(reader);
return new RangeOrValue(dto.Min, dto.Max);
}
}
}
Demo fiddle showing both converters here.
I have an IP address that I need to have as a 4 byte array in my code. However I would like to store it in my JSON settings file as a string, formatted like "192.168.0.1". Then I would also like to do the reverse and deserialize it.
I'd like to do this as the goal of my Settings.json file is that it is human editable.
Is there a way I can do this?
I'm using the Newtonsoft JSON package
Class I am serializing
public class Settings
{
public string PLCIP;
public byte[] RightTesterIP;
public byte[] LeftTesterIP;
}
converter methods I wrote. Just not sure where to implement them.
private string ConvertIPByteArraytoString(byte[] ip)
{
StringBuilder builder = new StringBuilder();
builder.Append(ip[0]);
for (int i = 1; i < ip.Length; i++)
{
builder.Append(".");
builder.Append(ip[i]);
}
return builder.ToString();
}
private byte[] ConvertIPStringToByteArray(string ip, string ParameterName)
{
var blah = new byte[4];
var split = ip.Split('.');
if (split.Length != 4)
{
//Log.Error("IP Address in settings does not have 4 octets.Number Parsed was {NumOfOCtets}", split.Length);
//throw new SettingsParameterException($"IP Address in settings does not have 4 octets. Number Parsed was {split.Length}");
}
for(int i = 0; i < split.Length; i++)
{
if(!byte.TryParse(split[i], out blah[i]))
{
//var ex = new SettingsParameterException($"Octet {i + 1} of {ParameterName} could not be parsed to byte. Contained \"{split[i]}\"");
//Log.Error(ex,"Octet {i + 1} of {ParameterName} could not be parsed to byte. Contained \"{split[i]}\"", i, ParameterName, split[i]);
//throw ex;
}
}
return blah;
}
You could do it in a custom JsonConverter like so:
public class IPByteArrayConverter : JsonConverter
{
private static string ConvertIPByteArraytoString(byte[] ip)
{
StringBuilder builder = new StringBuilder();
builder.Append(ip[0]);
for (int i = 1; i < ip.Length; i++)
{
builder.Append(".");
builder.Append(ip[i]);
}
return builder.ToString();
}
private static byte[] ConvertIPStringToByteArray(string ip)
{
var blah = new byte[4];
var split = ip.Split('.');
if (split.Length != 4)
{
//Log.Error("IP Address in settings does not have 4 octets.Number Parsed was {NumOfOCtets}", split.Length);
//throw new SettingsParameterException($"IP Address in settings does not have 4 octets. Number Parsed was {split.Length}");
}
for (int i = 0; i < split.Length; i++)
{
if (!byte.TryParse(split[i], out blah[i]))
{
//var ex = new SettingsParameterException($"Octet {i + 1} of {ParameterName} could not be parsed to byte. Contained \"{split[i]}\"");
//Log.Error(ex,"Octet {i + 1} of {ParameterName} could not be parsed to byte. Contained \"{split[i]}\"", i, ParameterName, split[i]);
//throw ex;
}
}
return blah;
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
if (token.Type == JTokenType.Bytes)
return (byte[])token;
return ConvertIPStringToByteArray((string)token);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var bytes = (byte[])value;
writer.WriteValue(ConvertIPByteArraytoString(bytes));
}
}
You would then attach it to the appropriate properties or fields using [JsonConverter(IPByteArrayConverter)]:
public class Settings
{
public string PLCIP;
[JsonConverter(typeof(IPByteArrayConverter))]
public byte[] RightTesterIP;
[JsonConverter(typeof(IPByteArrayConverter))]
public byte[] LeftTesterIP;
}
Sample fiddle.
Update
Using IPAddress as suggested by #Greg gets you support for IPV6 as well as IPV4. A JsonConverter for this type would look like:
public class IPAddressConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(IPAddress).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
if (token.Type == JTokenType.Bytes)
{
var bytes = (byte[])token;
return new IPAddress(bytes);
}
else
{
var s = (string)token;
return IPAddress.Parse(s);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var ip = (IPAddress)value;
writer.WriteValue(ip.ToString());
}
}
Then apply it to the Settings class as before, or use it globally in JsonSerializerSettings.Converters:
var jsonSettings = new JsonSerializerSettings
{
Converters = new [] { new IPAddressConverter() },
};
var json = JsonConvert.SerializeObject(settings, jsonSettings);
Using the class:
public class Settings
{
public string PLCIP;
public IPAddress RightTesterIP;
public IPAddress LeftTesterIP;
}
Sample fiddle.
When serialise, I write ClassName to object into _CurrentClassName property. And when read json with JSON.Net library I need to change object to value from this property.
{
"Field1": 0,
"Field2": "34",
"_CurrentClassName": "MyCustomClass"
}
class CustomJsonConverter : JsonConverter
{
...
public override bool CanConvert(Type objectType)
{
return objectType.IsClass;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = existingValue;
if (reader.TokenType == JsonToken.Null)
{
return null;
}
else if (reader.TokenType == JsonToken.StartObject)
{
JObject jObject = JObject.Load(reader);
JToken jToken;
if (jObject.TryGetValue("_CurrentClassName", out jToken))
{
var t = jToken.Value<string>();
Type tt = Type.GetType(objectType.Namespace + "." + t);
value = Activator.CreateInstance(tt);
return value;
}
}
return serializer.Deserialize(reader);
}
...
}
Once the object type has been inferred and the object has been instantiated, you can use JsonSerializer.Populate(jObject.CreateReader()) to populate it.
For instance:
public abstract class CustomJsonConverterBase : JsonConverter
{
protected abstract Type InferType(JToken token, Type objectType, object existingValue);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var token = JToken.Load(reader);
var actualType = InferType(token, objectType, existingValue);
if (existingValue == null || existingValue.GetType() != actualType)
{
var contract = serializer.ContractResolver.ResolveContract(actualType);
existingValue = contract.DefaultCreator();
}
using (var subReader = token.CreateReader())
{
// Using "populate" avoids infinite recursion.
serializer.Populate(subReader, existingValue);
}
return existingValue;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class CustomJsonConverter : CustomJsonConverterBase
{
public const string ClassPropertyName = #"_CurrentClassName";
protected override Type InferType(JToken token, Type objectType, object existingValue)
{
if (token is JObject)
{
var typeName = (string)token[ClassPropertyName];
if (typeName != null)
{
var actualType = Type.GetType(objectType.Namespace + "." + typeName);
if (actualType != null)
return actualType;
}
}
return objectType;
}
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
}
CustomCreationConverter<T> also uses serializer.Populate() to fill in the just-allocated object so this is a standard way to solve this problem.
Note that you are partially replicating Json.NET's built-in TypeNameHandling functionality.
If you're not married to the _CurrentClassName property name or its value syntax you could use Json.Net's built in handling of types.
When serializing or deserializing you can pass in a JsonSerializerSettings object controlling the serialization or deserialization.
On this object you can set a TypeNameHandling property that controls how Json.Net serializes and deserializes the exact type being processed.
Here is a LINQPad example:
void Main()
{
var t = new Test { Key = 42, Value = "Meaning of life" };
var json = JsonConvert.SerializeObject(
t, Newtonsoft.Json.Formatting.Indented,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
Console.WriteLine(json);
var obj =JsonConvert.DeserializeObject(json,
new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All });
Console.WriteLine(obj.GetType().FullName);
}
public class Test
{
public int Key { get; set; }
public string Value { get; set; }
}
Output:
{
"$type": "UserQuery+Test, LINQPadQuery",
"Key": 42,
"Value": "Meaning of life"
}
UserQuery+Test
Here you can see that the type of the object being returned from deserialization is the Test class.
JsonConvert.DeserializeObject successfully deserializes ['a','b'] as List<KeyValuePair<string, object>>. I would like it to fail, only succeeding when the input string is like [{'Key':'a','Value':'b'}].
Is there a way to accomplish this?
It appears you may have found a bug in Json.NET's KeyValuePairConverter, namely that it assumes the reader is positioned at the beginning of a JSON object rather than checking and validating that it is. You could report an issue on it if you want.
In the meantime, the following JsonConverter will correctly throw a JsonException for your case:
public class KeyValueConverter : JsonConverter
{
interface IToKeyValuePair
{
object ToKeyValuePair();
}
struct Pair<TKey, TValue> : IToKeyValuePair
{
public TKey Key { get; set; }
public TValue Value { get; set; }
public object ToKeyValuePair()
{
return new KeyValuePair<TKey, TValue>(Key, Value);
}
}
public override bool CanConvert(Type objectType)
{
bool isNullable = (Nullable.GetUnderlyingType(objectType) != null);
Type type = (Nullable.GetUnderlyingType(objectType) ?? objectType);
return type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);
}
public override bool CanWrite { get { return false; } } // Use Json.NET's writer.
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
bool isNullable = (Nullable.GetUnderlyingType(objectType) != null);
Type type = (Nullable.GetUnderlyingType(objectType) ?? objectType);
if (isNullable && reader.TokenType == JsonToken.Null)
return null;
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
var pairType = typeof(Pair<,>).MakeGenericType(type.GetGenericArguments());
var pair = serializer.Deserialize(reader, pairType);
if (pair == null)
return null;
return ((IToKeyValuePair)pair).ToKeyValuePair();
}
else
{
throw new JsonSerializationException("Invalid type: " + objectType);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then use it like:
string json = #"['a','b']";
var settings = new JsonSerializerSettings { Converters = new JsonConverter[] { new KeyValueConverter() } };
var list = JsonConvert.DeserializeObject<List<KeyValuePair<string, object>>>(json, settings);
Example fiddle.
Update
To force an error when the JSON contains a non-existent property, use JsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Error.
var settings = new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error,
Converters = new JsonConverter[] { new KeyValueConverter() },
};