I am using JSON.net and am trying to serialize and deserialize a Distance object from the opensource UnitClassLibrary. Currently, I have an object serialized as the following JSON:
{
"ThirtySecondsOfAnInch": 454,
"SixteenthsOfAnInch": 227,
"EighthsOfAnInch": 113.5,
"QuartersOfAnInch": 56.75,
"HalvesOfAnInch": 28.375,
"Inches": 14.1875,
"Feet": 1.1822916666666667,
"Yards": 0.3940972222222222,
"Miles": 0.00022391887626262627,
"Millimeters": 360.36249999999995,
"Centimeters": 36.03625,
"Meters": 0.3603625,
"Kilometers": 0.0003603625,
"Architectural": "1'2 3/16\""
}
I can take any one of these and turn it into a distance object using this class. For example, using the last architectural string, I could use this constructor:
Distance newDistance = new Distance("1'2 3/16\"");
Or, using the 32nd of a inch, I could do:
Distance newDistance = new Distance(DistanceType.ThirtySecond, 454.0);
However, I am not sure the best way to write a JsonConstructor (the type of constructor that JSON.net can specifically use) to take either type of output upon passing a JSON string with JsonConvert.DeserializeObject<Distance>(json);.
How can I write a constructor to take a new distance object?
Here's an example JsonConverter to get you started.
public class DistanceConverter : JsonConverter
{
private readonly IDictionary<string, DistanceType> _distanceTypeMap;
public DistanceConverter()
{
_distanceTypeMap = new Dictionary<string, DistanceType>
{
{"Meters", DistanceType.Meter},
{"Yards", DistanceType.Yard}
};
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Distance distance = value as Distance;
if (distance == null)
{
writer.WriteNull();
return;
}
writer.WriteStartObject();
foreach (KeyValuePair<string, DistanceType> pair in _distanceTypeMap)
{
writer.WritePropertyName(pair.Key);
writer.WriteValue(distance.GetValue(pair.Value));
}
writer.WriteEndObject();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Distance result = null;
while (reader.Read())
{
var key = reader.Value;
string value = reader.ReadAsString();
if (result == null && key != null)
{
DistanceType distanceType;
if (_distanceTypeMap.TryGetValue(key.ToString(), out distanceType))
{
double parsedValue = JToken.Parse(value).Value<double>();
result = new Distance(distanceType, parsedValue);
}
}
}
return result;
}
public override bool CanConvert(Type objectType)
{
return typeof(Distance) == objectType;
}
}
This has problems at the moment. I couldn't understand why do I get zero distance even if I create the object in deserialization. Here's a few tests to see the results:
Update: I fixed the bug while reading it again. It works at the moment but you may get exceptions when you add other units to the dictionary.
public class DistanceConverterTests
{
private JsonSerializerSettings _jsonSerializerSettings;
[SetUp]
public void Setup()
{
_jsonSerializerSettings = new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new DistanceConverter() }
};
}
[Test]
public void DeserializeTest()
{
string json = File.ReadAllText("data.json");
var distance = JsonConvert.DeserializeObject<Distance>(json, _jsonSerializerSettings);
distance.Meters.Should().BeInRange(0.360, 0.361);
}
[Test]
public void SerializeTest()
{
Distance distance = new Distance(DistanceType.Meter, 2.20);
string json = JsonConvert.SerializeObject(distance, _jsonSerializerSettings);
Console.WriteLine(json);
}
}
Related
I have the following model:
public class UserPtr
{
public int my_var1 { get; set; }
public int my_var2 { get; set; }
public int my_var3 { get; set; }
public int my_var4 { get; set; }
}
And some API response JSON which is:
[
{
"name": "my_var1",
"ptr": 1 // "Value_my_var1"
},
{
"name": "my_var2",
"ptr": 2 // "Value_my_var2"
},
{
"name": "my_var3",
"ptr": 3 // "Value_my_var3"
},
{
"name": "my_var4",
"ptr": 4 // "Value_my_var4"
}
]
I want to set my_var1 = Value_my_var1, my_var2 = Value_my_var2, my_var3 = Value_my_var3
Normally I would use:
JsonConvert.DeserializeObject<UserPtr>(strJson);
But when I do, I get the following exception:
Newtonsoft.Json.JsonSerializationException: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'UserPtr' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
How can I deserialize this array of objects containing property names and values into my model?
You would like to serialize your model as an array of objects containing property names and property values, where the names and values come from the "default" JSON serialization for your model. You can do this with a custom generic JsonConverter<T> that translates between the default serialization and the array serialization.
By default, your UserPtr model should be serialized as follows:
{
"my_var1": 1,
"my_var2": 2,
"my_var3": 2,
"my_var4": 4
}
But instead, you are receiving an array of objects containing single name/value pairs as shown in your question, where the names correspond to your model's property names. You would like to bind this array to your model. To accomplish this, you can create a generic converter similar to the one from Deserialize JSON from a Sharepoint 2013 search result into a list of MyClass as follows:
public class NamePtrPropertyArrayConverter<T> : JsonConverter<T> where T : class, new()
{
struct NamePtrDTO
{
public string name;
public object ptr;
}
public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
{
var obj = (JObject)JsonExtensions.DefaultFromObject(serializer, value);
serializer.Serialize(writer, obj.Properties().Select(p => new NamePtrDTO { name = p.Name, ptr = p.Value }));
}
public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.MoveToContentAndAssert().TokenType == JsonToken.Null)
return null;
var array = serializer.Deserialize<List<NamePtrDTO>>(reader);
var obj = new JObject(array.Select(i => new JProperty(i.name, i.ptr)));
existingValue = existingValue ?? (T)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
using (var subReader = obj.CreateReader())
serializer.Populate(subReader, existingValue);
return existingValue;
}
}
public static partial class JsonExtensions
{
public static JsonReader MoveToContentAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
reader.ReadAndAssert();
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
reader.ReadAndAssert();
return reader;
}
public static JsonReader ReadAndAssert(this JsonReader reader)
{
if (reader == null)
throw new ArgumentNullException();
if (!reader.Read())
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
// DefaultFromObject() taken from this answer https://stackoverflow.com/a/29720068/3744182
// By https://stackoverflow.com/users/3744182/dbc
// To https://stackoverflow.com/questions/29719509/json-net-throws-stackoverflowexception-when-using-jsonconvert
public static JToken DefaultFromObject(this JsonSerializer serializer, object value)
{
if (value == null)
return JValue.CreateNull();
var dto = Activator.CreateInstance(typeof(DefaultSerializationDTO<>).MakeGenericType(value.GetType()), value);
var root = JObject.FromObject(dto, serializer);
return root["Value"].RemoveFromLowestPossibleParent() ?? JValue.CreateNull();
}
public static JToken RemoveFromLowestPossibleParent(this JToken node)
{
if (node == null)
return null;
// If the parent is a JProperty, remove that instead of the token itself.
var contained = node.Parent is JProperty ? node.Parent : node;
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (contained is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}
interface IHasValue
{
object GetValue();
}
[JsonObject(NamingStrategyType = typeof(DefaultNamingStrategy), IsReference = false)]
class DefaultSerializationDTO<T> : IHasValue
{
public DefaultSerializationDTO(T value) => this.Value = value;
public DefaultSerializationDTO() { }
[JsonConverter(typeof(NoConverter)), JsonProperty(ReferenceLoopHandling = ReferenceLoopHandling.Serialize)]
public T Value { get; set; }
object IHasValue.GetValue() => Value;
}
}
public class NoConverter : JsonConverter
{
// NoConverter taken from this answer https://stackoverflow.com/a/39739105/3744182
// By https://stackoverflow.com/users/3744182/dbc
// To https://stackoverflow.com/questions/39738714/selectively-use-default-json-converter
public override bool CanConvert(Type objectType) { throw new NotImplementedException(); /* This converter should only be applied via attributes */ }
public override bool CanRead => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
public override bool CanWrite => false;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException();
}
Then, either deserialize by adding the converter to JsonSerializerSettings.Converters:
var settings = new JsonSerializerSettings
{
Converters = { new NamePtrPropertyArrayConverter<UserPtr>() },
};
var model = JsonConvert.DeserializeObject<UserPtr>(strJson, settings);
Or apply the converter directly to your model as follows:
[JsonConverter(typeof(NamePtrPropertyArrayConverter<UserPtr>))]
public class UserPtr
{
// Contents unchanged
}
Demo fiddle here.
I have a dictionary that looks like this that gets Deserialized from a json file to a C# Dictionary
{
"Name": "option1",
"Settings": {
"setting1": 20,
"setting2": 2
}
}
So I Deserialize this to an object which works but when i try to get that value out of the dictionary it becomes a long instead of an int
if (!settings.TryGetValue("setting1", out object setting))
{
setting = 10;
}
Then somewhere else I do something like this:
if ((int)setting > 10) {//do something}
Then it gives me the error that an int64 cant be converted to an int32
Deserialization process:
using (StreamReader reader = new StreamReader(Startup.SettingsPath))
{
SettingsModel settings = JsonConvert.DeserializeObject<SettingsModel>(reader.ReadToEnd()).Applications;
return settings
}
SettingsModel:
public class SettingsModel
{
public string Name { get; set; }
public IDictionary<string, object> Settings { get; set; }
}
If you really need to deserialize the number as an integer, you could implement a custom JsonConverter as:
public class CustomConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(object).Equals(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken jToken = JValue.ReadFrom(reader);
switch (reader.TokenType)
{
case JsonToken.Integer:
return jToken.Value<int>();
case JsonToken.String:
return jToken.Value<string>();
// ...and so on...
default:
throw new ArgumentException($"Unknown JsonToken: '{reader.TokenType}'.");
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
}
}
Pass an instance of the custom converter class to the DeserializeObject() method as:
JsonConvert.DeserializeObject<SettingsModel>(reader.ReadToEnd(), new CustomConverter());
I had an instance that required a one off for this situation. I implemented the following.
private int GetInt(KeyValuePair<string, object> pair)
{
object value = pair.Value;
Type valueType = value.GetType();
if (property.PropertyType.IsAssignableFrom(typeof(int)))
{
value = Convert.ToInt32(value);
}
}
I'm attempting to implement IDeserializationCallback using JSON.NET. I'm deserializing an object, and I would like to generate a list of all the objects which were deserialized which implement IDeserializationCallback, what would be the best way to do this? Does JSON.NET have any appropriate extension point to facilitate this? I have a (seemingly) working solution below, however it is quite ugly, so I'm convinced there must be a better way to do this. Any help is appreciated, thanks!
private static JsonSerializer serializer = new JsonSerializer();
static cctor()
{
serializer.Converters.Add(new DeserializationCallbackConverter());
}
public static T Deserialize<T>(byte[] data)
{
using (var reader = new JsonTextReader(new StreamReader(new MemoryStream(data))))
using (DeserializationCallbackConverter.NewDeserializationCallbackBlock(reader))
return serializer.Deserialize<T>(reader);
}
private class DeserializationCallbackConverter : JsonConverter
{
[ThreadStatic]
private static ScopedConverter currentConverter;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
return currentConverter.ReadJson(reader, objectType, serializer);
}
public override bool CanConvert(Type objectType)
{
return currentConverter == null ? false : currentConverter.CanConvert();
}
public override bool CanWrite
{
get { return false; }
}
public static IDisposable NewDeserializationCallbackBlock(JsonReader reader)
{
return new ScopedConverter(reader);
}
private class ScopedConverter : IDisposable
{
private JsonReader jsonReader;
private string currentPath;
private List<IDeserializationCallback> callbackObjects;
public ScopedConverter(JsonReader reader)
{
jsonReader = reader;
callbackObjects = new List<IDeserializationCallback>();
currentConverter = this;
}
public object ReadJson(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var lastPath = currentPath;
currentPath = reader.Path;
var obj = serializer.Deserialize(reader, objectType);
currentPath = lastPath;
var dc = obj as IDeserializationCallback;
if (dc != null && callbackObjects != null)
callbackObjects.Add(dc);
return obj;
}
public bool CanConvert()
{
return jsonReader.Path != currentPath;
}
public void Dispose()
{
currentConverter = null;
foreach (var obj in callbackObjects)
obj.OnDeserialization(null);
}
}
}
You can create a custom contract resolver that adds an extra, artificial OnDeserialized callback that tracks creation of reference type objects. Here's one example:
public interface IObjectCreationTracker
{
void Add(object obj);
ICollection<object> CreatedObjects { get; }
}
public class ReferenceObjectCreationTracker : IObjectCreationTracker
{
public ReferenceObjectCreationTracker()
{
this.CreatedObjects = new HashSet<object>();
}
public void Add(object obj)
{
if (obj == null)
return;
var type = obj.GetType();
if (type.IsValueType || type == typeof(string))
return;
CreatedObjects.Add(obj);
}
public ICollection<object> CreatedObjects { get; private set; }
}
public class ObjectCreationTrackerContractResolver : DefaultContractResolver
{
readonly SerializationCallback callback = (o, context) =>
{
var tracker = context.Context as IObjectCreationTracker;
if (tracker != null)
tracker.Add(o);
};
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
contract.OnDeserializedCallbacks.Add(callback);
return contract;
}
}
And then use it as follows:
public static class JsonExtensions
{
public static T DeserializeWithTracking<T>(string json, out ICollection<object> objects)
{
var tracker = new ReferenceObjectCreationTracker();
var settings = new JsonSerializerSettings
{
ContractResolver = new ObjectCreationTrackerContractResolver(),
Context = new StreamingContext(StreamingContextStates.All, tracker),
// Add other settings as required.
TypeNameHandling = TypeNameHandling.Auto,
};
var obj = (T)JsonConvert.DeserializeObject<T>(json, settings);
objects = tracker.CreatedObjects;
return obj;
}
}
Note that this only returns instances of non-string reference types. Returning instances of value types is more problematic as there is no obvious way to distinguish between a value type that eventually gets embedded into a larger object via a property setter and one that is retained in the object graph as a boxed reference, e.g. as shown in this question. If the boxed value type eventually gets embedded in some larger object there is no way to retain a direct reference to it.
Also note the use of StreamingContext.Context to pass the tracker down into the callback.
You may want to cache the contract resolver for best performance.
Update
In answer to the updated question of how to implement IDeserializationCallback with Json.NET, the above should work for reference types. For value types that implement this interface, you could:
Call the method immediately in the OnDeserialized callback rather than deferring it until serialization is complete, or
Throw an exception indicating that IDeserializationCallback is not supported for structs.
Using Json.net, deserializing a type that contains a Tuple<...> doesn't work (serialization works, but deserialization doesn't):
[TestMethod]
public void Test()
{
var orig = new TupleHolder("what????", true);
var json = JsonConvert.SerializeObject(orig);
Assert.AreEqual("{\"Tup\":{\"Item1\":\"what????\",\"Item2\":true}}", json);
// great! serialization works like a charm! now let's test deserialization:
var dupl = JsonConvert.DeserializeObject<TupleHolder>(json);
Assert.AreEqual("ZZZ", dupl.Tup.Item1); // pass! but it should be "what????"... what????
Assert.AreEqual(false, dupl.Tup.Item2); // pass! but it should be "true", right???
Assert.AreEqual(orig.Tup.Item1, dupl.Tup.Item1); // fail!
Assert.AreEqual(orig.Tup.Item2, dupl.Tup.Item2); // fail!
}
public class TupleHolder
{
public Tuple<string, bool> Tup { get; set; }
public TupleHolder() { Tup = new Tuple<string, bool>("ZZZ", false); }
public TupleHolder(string s, bool b) { Tup = new Tuple<string, bool>(s, b); }
}
Funny thing is that direct deserialization of Tuple<...> does work:
[TestMethod]
public void Test2()
{
var orig = new Tuple<string, bool>("ABC", true);
var json = JsonConvert.SerializeObject(orig);
var dupl = JsonConvert.DeserializeObject<Tuple<string, bool>>(json);
Assert.AreEqual(orig, dupl); // direct deserialization of Tuple<...> works.
}
Is it a Json.NET bug or am I missing here something?
The answer provided by Remi helped me. I took his TupleConverter and made it generic for a 2-tuple. The concept is the same for any N-tuple.
I leave it here in case it helps someone.
public class TupleConverter<U, V> : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Tuple<U, V>) == objectType;
}
public override object ReadJson(
Newtonsoft.Json.JsonReader reader,
Type objectType,
object existingValue,
Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
return null;
var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var target = new Tuple<U, V>(
jObject["m_Item1"].ToObject<U>(), jObject["m_Item2"].ToObject<V>());
return target;
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
Note: My Tuple was JSON serialized with m_Item1 and m_Item2, so I had to change jObject["ItemX"] to jObject["m_ItemX"]
Usage example with a List<Tuple<int, User>>:
string result = "String to deserialize";
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new TupleConverter<int, User>());
List<Tuple<int, User>> users = JsonConvert.DeserializeObject<List<Tuple<int, User>>>(result, settings);
The solution - or mine, anyhow - is to define a custom converter for the Tuple.
This example provides a concrete solution for a specific Tuple, but you could genericize it to make the TupleConverter class to handle any combination of value types. Could also make it abstract and have derived types implement instantiation methods for each item, to handle tuples with reference types.
public class TupleConverter : Newtonsoft.Json.JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Tuple<string, bool>) == objectType;
}
public override object ReadJson(
Newtonsoft.Json.JsonReader reader,
Type objectType,
object existingValue,
Newtonsoft.Json.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.Json.JsonToken.Null)
return null;
var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var target = new Tuple<string, bool>(
(string)jObject["Item1"], (bool)jObject["Item2"]);
return target;
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
public class TupleHolder
{
[Newtonsoft.Json.JsonConverter(typeof(TupleConverter))]
public Tuple<string, bool> Tup { get; set; }
public TupleHolder() { Tup = new Tuple<string, bool>("ZZZ", false); }
public TupleHolder(string s, bool b) { Tup = new Tuple<string, bool>(s, b); }
}
[Test]
public void Test()
{
var orig = new TupleHolder("what????", true);
var json = Newtonsoft.Json.JsonConvert.SerializeObject(orig);
Assert.AreEqual("{\"Tup\":{\"Item1\":\"what????\",\"Item2\":true}}", json);
var dupl = Newtonsoft.Json.JsonConvert.DeserializeObject<TupleHolder>(json);
// These succeed, now
Assert.AreEqual(orig.Tup.Item1, dupl.Tup.Item1);
Assert.AreEqual(orig.Tup.Item2, dupl.Tup.Item2);
}
I ended up with something more generic, hope it helps
public class TupleConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
var match = Regex.Match(objectType.Name, "Tuple`([0-9])", RegexOptions.IgnoreCase);
return match.Success;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
try
{
var tupleTypes = objectType.GetProperties().ToList().Select(p => p.PropertyType).ToArray();
var jObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var valueItems = new List<object>();
for (var i = 1; i <= tupleTypes.Length; i++)
valueItems.Add(jObject[$"m_Item{i}"].ToObject(tupleTypes[i - 1]));
var convertedObject = objectType.GetConstructor(tupleTypes)?.Invoke(valueItems.ToArray());
return convertedObject;
}
catch (Exception ex)
{
throw new Exception("Something went wrong in this implementation", ex);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
I have created a test console application that has a simple class named Other. As an exercise, I want to set the OtherString property to null when it is serialized. I know how I could do this with a custom ContractResolver. I need to be able to do this with a custom Converter too.
The first converter I wrote was simple and the way I thought it should be. However, it would throw a "Self referencing loop detected with type 'JsonContractandConvert.Models.Other'. Path ''." exception. After doing some reading, I made some changes and now I have a working converter. These changes are a lot more verbose, but work.
My question is why are these changes required and is there a better way to do this with a Converter?
Other Class:
[JsonConverter(typeof(OtherConverter))]
public class Other
{
public int Id { get; set; }
public string OtherString { get; set; }
public int OtherInt { get; set; }
public string OtherName
{
get
{
return "Other Name = " + this.OtherString;
}
}
}
First Attempt: (This one throws an exception)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
var entity = value as Other;
if (entity == null) return;
entity.OtherString = null;
serializer.Serialize(writer, entity);
}
Second Attempt: (This works as expected)
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
var entity = value as Other;
if (entity == null) return;
entity.OtherString = null;
writer.WriteStartObject();
var props = entity.GetType().GetProperties();
foreach (var propertyInfo in props)
{
var ignorAttribute =
propertyInfo.CustomAttributes.FirstOrDefault(i => i.AttributeType == typeof(JsonIgnoreAttribute));
if (ignorAttribute != null) continue;
var tempVal = propertyInfo.GetValue(entity);
if (tempVal == null) continue;
writer.WritePropertyName(propertyInfo.Name);
serializer.Serialize(writer, tempVal);
}
}
writer.WriteEndObject();
}
Edit:
Here's the code from the console app that I'm using to test.
class Program
{
static void Main(string[] args)
{
var otherObj = new Other { Id = 123, OtherInt = 456, OtherString = "This is the other string"};
var json = JsonConvert.SerializeObject(otherObj, Formatting.Indented);
Console.WriteLine(json);
}
}
Answering my own questions... (I think I've finally got my head around Json Converters)
To answer my basic question as to why one way works and the other doesn't. I think the answer is just that the way that doesn't work is just wrong. From what I can tell, you need to use the writer object. If you don't it won't work. (There may be scenarios where this is not the case, but I never found one.)
If you merely want to remove a property from the Json, use [JsonIgnore]. (Simple right?)
If you want to change the value some how, you'll need to use a Converter. By using a Converter, you're taking responsibility for serializing what ever object the Converter handles. So if the object is a simple type like a String, or an Int it's really easy. If it's something more complex like an array or complex object, then it takes a little more planning.
Here's an example of a simple String Converter:
Class to serialize:
public class Account
{
public int Id { get; set; }
public string AccountName { get; set; }
[JsonIgnore]
public virtual Account DefaultAssignTo { get; set; }
public int? DefaultAssignToId { get; set; }
[JsonIgnore]
public virtual ICollection<Role> Roles { get; set; }
[JsonIgnore]
public virtual Other Other { get; set; }
public int? OtherId { get; set; }
[JsonConverter(typeof(StringConverter))]
public string OtherName
{
get
{
return "Name = " + this.AccountName;
}
}
}
Converter:
public class StringConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(string).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
writer.WriteValue("blah blah blah");
}
}
Run: (As you can see I'm using Entity Framework for my data)
using (var db = new Context())
{
var json = JsonConvert.SerializeObject(db.Accounts.FirstOrDefault(), Formatting.Indented,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
Console.WriteLine(json);
}
Returns:
{
"Id": 43,
"AccountName": "John",
"DefaultAssignToId": 43,
"OtherId": 19,
"OtherName": "blah blah blah"
}
One of the things that messed me up for a bit was when to use writer.WriteStartObject(). Basically if you are Converting a complex object, you need to use it. If you do, then you need to create all of the property names and values. In the OP you can see an example of how I did this. The biggest down side is any Json attributes that properties are decorated with don't automatically happen. So if you can, decorate the class and let the serializer deal with it. Here's an interesting problem you can run into if you use it wrong.
In the String converter replace:
writer.WriteValue("blah blah blah");
with:
writer.WriteStartObject();
writer.WritePropertyName("BlahProp");
serializer.Serialize(writer, "blah blah blah");
writer.WriteEndObject();
run it again and here's the output: (Notice how OtherName is an object now instead of a string)
{
"Id": 43,
"AccountName": "John",
"DefaultAssignToId": 43,
"OtherId": 19,
"OtherName": {
"BlahProp": "blah blah blah"
}
}
The Roles collection also needed a Converter. Two things to note are first, you don't need to specify the property name and second, any Json attributes that the Role class is decorated with will work as expected.
using BaseCollection = System.Collections.Generic.ICollection<JsonContractandConvert.Models.Role>;
public class RemoveAccountsFromRolesConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(BaseCollection).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (!this.CanConvert(value.GetType())) return;
var entities = value as BaseCollection;
if (entities == null) return;
writer.WriteStartArray();
foreach (var entity in entities)
{
entity.Accounts = null;
serializer.Serialize(writer, entity);
}
writer.WriteEndArray();
}
}
Lastly, here's a couple of converters I created for my real project:
This one converts an object to a shallow copy. This is useful if you have a collection where what that collection contains could cause a self referencing loop.
public class ShallowCopyCollectionConverter<TCollectionType, TCopyType> : JsonConverter
where TCollectionType : IEnumerable<TbdEntity>
where TCopyType : TbdEntity, new()
{
public override bool CanConvert(Type objectType)
{
return typeof(TCollectionType).IsAssignableFrom(objectType);
}
public override bool CanRead
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (this.CanConvert(value.GetType()) == false) return;
var entities = (TCollectionType)value;
writer.WriteStartArray();
foreach (var entity in entities)
{
serializer.Serialize(writer, entity.ShallowCopy<TCopyType>()); //ShallowCopy<> is a method in the base class that all of my classes extend.
}
writer.WriteEndArray();
}
}
This one will convert an abstract using the concrete type:
public class DataSnapInConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(DataSnapIn) == (objectType);
}
public override bool CanWrite
{
get { return false; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (this.CanConvert(objectType) == false) return null;
var jo = JObject.Load(reader);
var typeName = jo["snapInType"] ?? jo["SnapInType"]; //the abstract classes have this property to identify what concrete class they are.
var typeNameString = typeName.ToString();
var deserializeType = Type.GetType(typeNameString);
if(deserializeType == null)
throw new Exception("SnapInType is null or does not reference a valid class.");
var result = Activator.CreateInstance(deserializeType);
serializer.Populate(jo.CreateReader(), result);
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}