Lets say I have a model like:
public class MyModel
{
public string Name { get; set; }
public string[] Size { get; set; }
public string Weight { g
You can do it with the following converter:
public class MyModelConverter : JsonConverter
{
[ThreadStatic]
static bool cannotWrite;
// Disables the converter in a thread-safe manner.
bool CannotWrite { get { return cannotWrite; } set { cannotWrite = value; } }
public override bool CanWrite { get { return !CannotWrite; } }
public override bool CanConvert(Type objectType)
{
return typeof(MyModel).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var obj = JObject.Load(reader);
obj.SelectToken("details.size").MoveTo(obj);
obj.SelectToken("details.weight").MoveTo(obj);
using (reader = obj.CreateReader())
{
// Using "populate" avoids infinite recursion.
existingValue = (existingValue ?? new MyModel());
serializer.Populate(reader, existingValue);
}
return existingValue;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Disabling writing prevents infinite recursion.
using (new PushValue<bool>(true, () => CannotWrite, val => CannotWrite = val))
{
var obj = JObject.FromObject(value, serializer);
var details = new JObject();
obj.Add("details", details);
obj["size"].MoveTo(details);
obj["weight"].MoveTo(details);
obj.WriteTo(writer);
}
}
}
public static class JsonExtensions
{
public static void MoveTo(this JToken token, JObject newParent)
{
if (newParent == null)
throw new ArgumentNullException();
if (token != null)
{
if (token is JProperty)
{
token.Remove();
newParent.Add(token);
}
else if (token.Parent is JProperty)
{
token.Parent.Remove();
newParent.Add(token.Parent);
}
else
{
throw new InvalidOperationException();
}
}
}
}
public struct PushValue<T> : IDisposable
{
Action<T> setValue;
T oldValue;
public PushValue(T value, Func<T> getValue, Action<T> setValue)
{
if (getValue == null || setValue == null)
throw new ArgumentNullException();
this.setValue = setValue;
this.oldValue = getValue();
setValue(value);
}
#region IDisposable Members
// By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
public void Dispose()
{
if (setValue != null)
setValue(oldValue);
}
#endregion
}
And then use it like this:
[JsonConverter(typeof(MyModelConverter))]
public class MyModel
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("size")]
public string[] Size { get; set; }
[JsonProperty("weight")]
public string Weight { get; set; }
}
public class TestClass
{
public static void Test()
{
string json = @"{
""name"" : ""widget"",
""details"" : {
""size"" : [
""XL"",""M"",""S"",
],
""weight"" : ""heavy""
}
}";
var mod = JsonConvert.DeserializeObject<MyModel>(json);
Debug.WriteLine(JsonConvert.SerializeObject(mod, Formatting.Indented));
}
}
The ReadJson()
method is straightforward: deserialize to a JObject
, restructure the appropriate properties, then populate the MyModel
class. WriteJson
is a little more awkward; the converter needs to temporarily disable itself in a thread-safe manner to generate a "default" JObject
that can be then restructured.
You can simply use your model with an extra field for details
and use JsonIgnore
attribute to ignore serialization of Size
and Weight
fields. So your model will look like this:
public class MyModel
{
[JsonProperty("name")]
public string Name { get; set; }
public Details details { get; set; }
[JsonIgnore]
public string[] Size
{
get
{
return details != null ? details.size : null;
}
set
{
if (details == null)
{
details = new Details();
}
details.size = value;
}
}
[JsonIgnore]
public string Weight
{
get
{
return details != null ? details.weight : null;
}
set
{
if (details == null)
{
details = new Details();
}
details.weight = value;
}
}
}
then you can simply serialize/deserialize you model like this:
var deserializedModel = JsonConvert.DeserializeObject<MyModel>("your json string...");
var myModel = new MyModel { Name = "widget", Size = new[] { "XL", "M", "S" }, Weight = "heavy" };
string serializedObject = JsonConvert.SerializeObject(myModel);
This should work:
public class MyModelJsonConverter : JsonConverter
{
public override bool CanRead
{
get { return true; }
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(MyModel);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (objectType != typeof(MyModel))
{
throw new ArgumentException("objectType");
}
switch (reader.TokenType)
{
case JsonToken.Null:
{
return null;
}
case JsonToken.StartObject:
{
reader.Read();
break;
}
default:
{
throw new JsonSerializationException();
}
}
var result = new MyModel();
bool inDetails = false;
while (reader.TokenType == JsonToken.PropertyName)
{
string propertyName = reader.Value.ToString();
if (string.Equals("name", propertyName, StringComparison.OrdinalIgnoreCase))
{
reader.Read();
result.Name = serializer.Deserialize<string>(reader);
}
else if (string.Equals("size", propertyName, StringComparison.OrdinalIgnoreCase))
{
if (!inDetails)
{
throw new JsonSerializationException();
}
reader.Read();
result.Size = serializer.Deserialize<string[]>(reader);
}
else if (string.Equals("weight", propertyName, StringComparison.OrdinalIgnoreCase))
{
if (!inDetails)
{
throw new JsonSerializationException();
}
reader.Read();
result.Weight = serializer.Deserialize<string>(reader);
}
else if (string.Equals("details", propertyName, StringComparison.OrdinalIgnoreCase))
{
reader.Read();
if (reader.TokenType != JsonToken.StartObject)
{
throw new JsonSerializationException();
}
inDetails = true;
}
else
{
reader.Skip();
}
reader.Read();
}
if (inDetails)
{
if (reader.TokenType != JsonToken.EndObject)
{
throw new JsonSerializationException();
}
reader.Read();
}
return result;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
var model = value as MyModel;
if (model == null) throw new JsonSerializationException();
writer.WriteStartObject();
writer.WritePropertyName("name");
writer.WriteValue(model.Name);
writer.WritePropertyName("details");
writer.WriteStartObject();
writer.WritePropertyName("size");
serializer.Serialize(writer, model.Size);
writer.WritePropertyName("weight");
writer.WriteValue(model.Weight);
writer.WriteEndObject();
writer.WriteEndObject();
}
}
[JsonConverter(typeof(MyModelJsonConverter))]
public class MyModel
{
public string Name { get; set; }
public string[] Size { get; set; }
public string Weight { get; set; }
}
With the attribute on the class, using it is as easy as:
var model = new MyModel
{
Name = "widget",
Size = new[] { "XL", "M", "S" },
Weight = "heavy"
};
string output = JsonConvert.SerializeObject(model);
// {"name":"widget","details":{"size":["XL","M","S"],"weight":"heavy"}}
var model2 = JsonConvert.DeserializeObject<MyModel>(output);
/*
{
Name = "widget",
Size = [ "XL", "M", "S" ],
Weight = "heavy"
}
*/