Your question basically the reverse of the question Can I serialize nested properties to my class in one operation with Json.net?, and can be solved with a similar strategy. To simplify things, create an interface for all classes containing common fields:
public interface IHasCommonFields
{
CommonFields Common { get; set; }
}
Then you can create the following generic converter for any type implementing this interface:
public class HasCommonFieldsConverter<T> : JsonConverter where T : IHasCommonFields
{
[ThreadStatic]
static bool disabled;
// Disables the converter in a thread-safe manner.
bool Disabled { get { return disabled; } set { disabled = value; } }
public override bool CanWrite { get { return !Disabled; } }
public override bool CanRead { get { return !Disabled; } }
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
if (token == null || token.Type == JTokenType.Null)
return null;
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters
{
var hasCommon = token.ToObject<T>(serializer);
var common = (hasCommon.Common ?? (hasCommon.Common = new CommonFields()));
serializer.Populate(token.CreateReader(), common);
return hasCommon;
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters
{
var hasCommon = (T)value;
var obj = JObject.FromObject(hasCommon, serializer);
var common = hasCommon.Common;
if (common != null)
{
var commonObj = JObject.FromObject(common, serializer);
obj.Merge(commonObj);
}
obj.WriteTo(writer);
}
}
}
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
}
Then apply it to all classes implementing IHasCommonFields
- and in addition mark the Common
property with JsonIgnore
:
[JsonConverter(typeof(HasCommonFieldsConverter<Container1>))]
public class Container1 : IHasCommonFields
{
[JsonIgnore]
public CommonFields Common { get; set; }
[JsonProperty(PropertyName = "Name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "ProductId")]
public int ProductId { get; set; }
}
[JsonConverter(typeof(HasCommonFieldsConverter<Container2>))]
public class Container2 : IHasCommonFields
{
[JsonIgnore]
public CommonFields Common { get; set; }
[JsonProperty(PropertyName = "Group")]
public Guid Group { get; set; }
}
Then, to test:
public class TestClass
{
public static void Test()
{
var container1 = new Container1 { Name = "name", ProductId = 101, Common = new CommonFields { Id = "1401", Time = DateTime.Today.Ticks } };
var container2 = new Container2 { Group = Guid.NewGuid(), Common = new CommonFields { Id = "2401", Time = DateTime.Today.Ticks } };
Test(container1);
Test(container2);
}
private static void Test<T>(T container) where T : class, IHasCommonFields
{
var json = JsonConvert.SerializeObject(container, Formatting.Indented);
Debug.WriteLine(json);
var containerback = JsonConvert.DeserializeObject<T>(json);
var json2 = JsonConvert.SerializeObject(containerback, Formatting.Indented);
Debug.Assert(json == json2); // No assert
if (container.Common != null)
{
Debug.Assert(container.Common.Id == containerback.Common.Id); // No assert
Debug.Assert(container.Common.Time == containerback.Common.Time); // No assert
}
}
}
The JSON created looks like:
{
"Name": "name",
"ProductId": 101,
"Id": "1401",
"Time": 635725152000000000
}
And
{
"Group": "9ed31118-c0b7-4d9f-8f57-303b2e164643",
"Id": "2401",
"Time": 635725152000000000
}
The one disadvantage of this converter is that, if the Common
property is null when serialized, it will be deserialized non-null with default values.
If you don't want the IHasCommonFields
interface, you could e.g. create an abstract generic base class for your converter with abstract methods to get and set the common fields, then override those methods in each subclass.
(Honestly inheritance does seem simpler than composition here, as other answers have stated.)