2

I need to deserialize

{'Id': 'id123', 'Time': 1436231503, 'Name': 'foo', 'ProductId': 1}

into Container1

public class Container1
{
    public CommonFields Common { get; set; }

    //fields specific to Container1

    [JsonProperty(PropertyName = "Name")]
    public string Name { get; set; }

    [JsonProperty(PropertyName = "ProductId")]
    public int ProductId { get; set; }
}

and

{ 'Id': 'id123', 'Time': 1436231503, 'Group':'10768C21-9971-4D2F-ACD7-10C2EF19FCA8' }

into Container2

public class Container2
{
    public CommonFields Common { get; set; }

    //fields specific to Container2

    [JsonProperty(PropertyName = "Group")]
    public Guid Group { get; set; }
}

using composition (not inheritance). Both JSON have 2 common fields (Id and Time) and specific fields.

With newtonsoft.json JsonConvert.DeserializeObject<Container1>(json_container1) the result is that the properties of the 2 container are correctly deserialized. The common properties of the composed classed are not deserialized.

How can I deserialize JSON into C# classes that use only composition?

(using newtonsoft.json is not compulsory)

Below is my attempt.

public class CommonFields
{
    [JsonProperty(PropertyName = "Id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "Time")]
    public long Time { get; set; }
}

public class Container1
{
    public CommonFields Common { get; set; }
    [JsonProperty(PropertyName = "Name")]
    public string Name { get; set; }
    [JsonProperty(PropertyName = "ProductId")]
    public int ProductId { get; set; }
}

public class Container2
{
    public CommonFields Common { get; set; }
    [JsonProperty(PropertyName = "Group")]
    public Guid Group { get; set; }
}

internal class Program
{
    private static void Main(string[] args)
    {
        string json_container1 = @"{
'Id': 'id123',
'Time': 1436231503,
'Name': 'foo',
'ProductId': 1
 }";

        string json_container2 = @"{
'Id': 'id123',
'Time': 1436231503,
'Group':'10768C21-9971-4D2F-ACD7-10C2EF19FCA8'
 }";

        var container1Obj = JsonConvert.DeserializeObject<Container1>(json_container1); 

        var container2Obj = JsonConvert.DeserializeObject<Container2>(json_container2);

        Console.ReadKey();
}}}
robor
  • 2,969
  • 2
  • 31
  • 48
  • Why would you avoid using inheritance in the first place? You could have a base class containing the common fields and two children classes inheriting from it. – Arthur Rey Jul 15 '15 at 11:36

4 Answers4

1

Don't do it.

The JSON element you deserilize from should not be change, you can remove some properties but it's a bad practice to change its properties structure.

JSON file\content should have a compatible JSON class, if you want to make any changes, make another custom class and make a mapping logic between them.

Orel Eraki
  • 11,940
  • 3
  • 28
  • 36
0

I think you can just do the deserialize again just on CommonFields of two objects.

    container1Obj.Common = JsonConvert.DeserializeObject<CommonFields>(json_container1);
    container2Obj.Common = JsonConvert.DeserializeObject<CommonFields>(json_container2);
HarryQuake
  • 163
  • 1
  • 13
0

I know you want to use composition, but I really can't see any pros using composition over inheritance here.

public class BaseClass
{
    [JsonProperty(PropertyName = "Id")]
    public string Id { get; set; }

    [JsonProperty(PropertyName = "Time")]
    public long Time { get; set; }
}

public class Container1 : BaseClass
{
    [JsonProperty(PropertyName = "Name")]
    public string Name { get; set; }

    [JsonProperty(PropertyName = "ProductId")]
    public int ProductId { get; set; }
}

public class Container2 : BaseClass
{
    [JsonProperty(PropertyName = "Group")]
    public Guid Group { get; set; }
}

This is as simple as it can get and should get the job done.

Arthur Rey
  • 2,990
  • 3
  • 19
  • 42
0

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.)

dbc
  • 104,963
  • 20
  • 228
  • 340