Adding a new JSON property to a class from a Nuget

73 Views Asked by At

I work with a class that comes from a Nuget package, and therefore I can't add properties to it.

Imagine this class:

public class Base
{
    public Data Data { get; set; }

   // Many other properties..
}

public class Data
{
    public string Value1 { get; set; }

    // Many other properties..
}

The JSON payloads I work with look like this:

{
    Data:
    {
       "Value1": "some value...",
       "Value2": "some other value...",
       // Many other properties...
    }
}

In other words, the JSON payloads contain a property that does not exist in the Nuget's class - Value2.

I am looking for a way to deserialize the above JSON into a class, in a way that gives me access both to Base and its properties, and to Value2.

I am also in need of doing this both using Newtonsoft and System.Text.Json libraries.

I have tried creating a derived class, with a nested JsonProperty attribute ("Data/Value2") but that doesn't work with either library:

public class Derived : Base
{
    [JsonProperty("Data/Value2")]
    [JsonPropertyName("Data/Value2")]
    public string Value2 { get; set; }
}

I have tried overriding Data with in a derived class - that works in Newtonsoft but I lose the base class's Data, and it is not supported at all in System.Text.Json:

public class Derived : Base
{
    [JsonProperty("Data")]
    [JsonPropertyName("Data")]
    public Data Data2 { get; set; }
}

I have tried playing with some custom converters but haven't been able to find a working solution.

What is the way to support an extra property (one that can't be added to the class because it's external), without losing access to the class's existing properties, for both Newtonsoft and System.Text.Json libraries?

2

There are 2 best solutions below

0
Power Mouse On

I would not go with this approach, but will post for educational purposes. as mentioned in comments it would be simpler to serialize to class and map to another one. this example is showing Custom Json Converter . P.S. i wrote in a way, than you can put multiple Values3,4 etc. (still stiff code)

void Main()
{
    var x = new Derived() { Data = new Data() { Value1 = "xxx" }, Value2 = "aaaaa" };
    Console.WriteLine(x);
    var settings = new JsonSerializerSettings
    {
        Converters = { new CustomObjectConverter<Derived>() }
    };
    string json = JsonConvert.SerializeObject(x, 
                        Newtonsoft.Json.Formatting.Indented, 
                        new Newtonsoft.Json.JsonConverter[] { new CustomObjectConverter<Derived>() });
    Console.WriteLine(json);
}

#region classes
public class Base
{
    public Data Data { get; set; }

    // Many other properties..
}

public class Data
{
    public string Value1 { get; set; }
}

public class Derived : Base
{
    public string Value2 { get; set; }
}
#endregion classes

public class CustomObjectConverter<TModel> : Newtonsoft.Json.JsonConverter<TModel> where TModel : class, new()
{
    public override TModel? ReadJson(JsonReader reader, Type objectType, TModel? existingValue, bool hasExistingValue, Newtonsoft.Json.JsonSerializer serializer)
    {
        if (!(serializer.ContractResolver.ResolveContract(objectType) is JsonObjectContract contract))
            throw new NotImplementedException("Contract is not a JsonObjectContract");
        var jObj = JObject.Load(reader);

        var value = existingValue ?? (TModel?)contract.DefaultCreator?.Invoke() ?? new TModel();
        serializer.Populate(jObj.CreateReader(), value);
        return value;
    }

    public override bool CanWrite => true;
    public override void WriteJson(JsonWriter writer, TModel? value, Newtonsoft.Json.JsonSerializer serializer)
    {
        JObject jo = new JObject();
        Type type = value.GetType();
        List<JProperty> jp = new List<JProperty>();
        string[] FIELDS = new string[] {"Value2"}; //can have several properties

        foreach (PropertyInfo prop in type.GetProperties())
        {
            if (prop.CanRead)
            {
                object propVal = prop.GetValue(value, null);
                if (propVal != null)
                {
                    if (FIELDS.Contains(prop.Name))
                    {
                        jp.Add(new JProperty(prop.Name, propVal));
                    }
                    jo.Add(prop.Name, JToken.FromObject(propVal, serializer));
                }
            }
        }

        if (jp?.Count > 0)
        {
            jp.ForEach(fe => {
                jo.RemoveFields(new string[] {fe.Name});
                jo.SelectToken("Data").FirstOrDefault()?.AddAfterSelf(fe);
            });
        }
        jo.WriteTo(writer);
    }
}

public static class Ext
{
    public static JToken RemoveFields(this JToken token, string[] fields)
    {
        JContainer container = token as JContainer;
        if (container == null) return token;

        List<JToken> removeList = new List<JToken>();
        foreach (JToken el in container.Children())
        {
            JProperty p = el as JProperty;
            if (p != null && fields.Contains(p.Name))
            {
                removeList.Add(el);
            }
            el.RemoveFields(fields);
        }

        foreach (JToken el in removeList)
        {
            el.Remove();
        }

        return token;
    }
}

enter image description here

1
Gil On

Ended up using a DTO, as suggested in the comments. This appears to be required, at least until a new, relevant version of the Nuget is released.

Consider this example, when Base derives from BaseBase:

public class DerivedDTO : BaseBase
{
  public string Value2 { get; set; }

  // .. other properties
}

And when I deserialize I use the following:

public static class DerivedUtils
{
    public static Derived ToDerived(this DerivedDTO derivedDTO)
    {
        return new Derived
        {
            Value2 = derivedDTO.Value2;
            // ...
        };
    }
}