Search code examples
c#json.net

Serializing generic class with Newtonsoft without encapsulating inner type


Suppose the following Generic class and inner type class:

public class Generic<T>
    where T : class
{
    public bool IsSuccess { get; set; }
    public string Message { get; set; }
    public T Data { get; set; }
}

public class Specific
{
    public string Name { get; set; }
    public int Id { get; set; }
}

Whereby I can declare on object like:

var wrappedSpecific = new Generic<Specific>
{
    IsSuccess = true,
    Message = "Success",
    Data = new Specific
    {
        Name = "Test",
        Id = 1,
    }
};

Is there a way to have Newtonsoft/Json.NET serialize them without the encapsulation in "Data" like:

{"IsSuccess":true,"Message":"Success","Name":"Test","Id":1}

rather than:

{"IsSuccess":true,"Message":"Success","Data":{"Name":"Test","Id":1}}

The simple approach produces the encapsulation:

JsonConvert.SerializeObject(myGenericObject);

I know I could achieve this with classic inheritance (Specific : Generic) but I'm trying something else, I want to reuse my Specific class in contexts where I don't want the Generic attributes.


Solution

  • Yes, you do this by creating a custom JsonConverter. However, since Newtonsoft.Json doesn't like converters that take generics on attributes, you will need to pass in a DefaultContractResolver so it's able to use the new converter.

    The converter can be improved, it's just to show that it's possible.

    You also have to decorate your Generic with the JsonConverter attribute:

    [JsonConverter(typeof(GenericConverter<>))]
    public class Generic<T> where T : class
    {
        public bool IsSuccess { get; set; }
        public string Message { get; set; }
        public T Data { get; set; }
    }
    

    Converter:

    public class GenericConverter<T> : JsonConverter<Generic<T>> where T : class
    {
        public override void WriteJson(JsonWriter writer, Generic<T> value, JsonSerializer serializer)
        {
            JObject obj = new JObject(
                new JProperty("IsSuccess", value.IsSuccess),
                new JProperty("Message", value.Message)
            );
    
            // Check if Data is not null and add its properties to the JObject
            if (value.Data != null)
            {
                var dataObject = JObject.FromObject(value.Data, serializer);
                obj.Merge(dataObject, new JsonMergeSettings
                {
                    MergeArrayHandling = MergeArrayHandling.Union
                });
            }
            obj.WriteTo(writer);
        }
    
        public override Generic<T> ReadJson(JsonReader reader, Type objectType, Generic<T> existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    
        public override bool CanRead => false;
    }
    

    Contract resolver:

    public sealed class GenericConverterContractResolver : DefaultContractResolver
    {
        protected override JsonConverter ResolveContractConverter(Type objectType)
        {
            var typeInfo = objectType.GetTypeInfo();
            if (typeInfo.IsGenericType && !typeInfo.IsGenericTypeDefinition)
            {
                var jsonConverterAttribute = typeInfo.GetCustomAttribute<JsonConverterAttribute>();
                if (jsonConverterAttribute != null && jsonConverterAttribute.ConverterType.GetTypeInfo().IsGenericTypeDefinition)
                {
                    return (JsonConverter)Activator.CreateInstance(jsonConverterAttribute.ConverterType.MakeGenericType(typeInfo.GenericTypeArguments), jsonConverterAttribute.ConverterParameters);
                }
            }
            return base.ResolveContractConverter(objectType);
        }
    }
    

    Then you use it like this:

    var settings = new JsonSerializerSettings
    {
        ContractResolver = new GenericConverterContractResolver()
    };
    string json = JsonConvert.SerializeObject(genericInstance, settings);
    

    Update

    It is possible to skip the contract resolver, but then you have to pass in converters with the specific T used:

    var genericInstance = new Generic<GenericData>();
    var settings = new JsonSerializerSettings();
    settings.Converters.Add(new GenericConverter<GenericData>());
    string json = JsonConvert.SerializeObject(genericInstance, settings);