falcon
falcon

Reputation: 141

How to get the name of <T> from generic type and pass it into JsonProperty()?

I get the following error with the code below:

"An object reference is required for the non-static field, method, or property 'Response.PropName'"

Code:

public class Response<T> : Response
{
    private string PropName
    {
        get
        {
            return typeof(T).Name;
        }
    }            
    [JsonProperty(PropName)]
    public T Data { get; set; }
}

Upvotes: 14

Views: 2794

Answers (4)

Mickael V.
Mickael V.

Reputation: 1232

Here is how I did it using only System.Text.Json, adapted to your snippet :

public class Response<T>
{
    [JsonExtensionData]
    public Dictionary<string, JsonElement>? ExtraItems { get; set; }

    public T Data => ExtraItems[$"{typeof(T).Name.ToLowerInvariant()}"].Deserialize<T>();

}

For this to work if you have multiple properties and some are not of type T, the other properties have to be declared, and the only ones left are the generic ones (or I guess you could just add more lookup logic in the dict) ; like this :

public class PaginatedResponse<T>
{
    [JsonPropertyName("page")]
    public required PaginationInfo Pagination { get; set; }

    [JsonExtensionData]
    public Dictionary<string, JsonElement>? ExtraItems { get; set; }

    public T[] Items => ExtraItems[$"{typeof(T).Name.ToLowerInvariant()}s"].Deserialize<T[]>();

}

JsonExtensionData : https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/handle-overflow#handle-overflow-json

Upvotes: 0

Milton Hernandez
Milton Hernandez

Reputation: 664

@Thomas Levesque: OK. So let's say that you can't extend JObject in Response<T> because you need to extend a pre-existing Response class. Here's another way you could implement the same solution:

public class Payload<T> : Newtonsoft.Json.Linq.JObject  {
    private static string TypeName = (typeof(T)).Name;
    private T _data;

    public T Data {
        get { return _data; }
        set {
            _data = value;
            this[TypeName] = Newtonsoft.Json.Linq.JToken.FromObject(_data);
        }
    }
}

 //Response is a pre-existing class...
public class Response<T>: Response { 
    private Payload<T> Value;

    public Response(T arg)  {
        Value = new Payload<T>() { Data = arg };            
    }

    public static implicit operator JObject(Response<T> arg) {
        return arg.Value;
    }

    public string Serialize() {
        return Value.ToString();
    }
}

So now there are the following options to Serialize the class:

   static void Main(string[] args) {
        var p1 = new Response<Int32>(5);
        var p2 = new Response<string>("Message");
        JObject p3 = new Response<double>(0.0);
        var p4 = (JObject) new Response<DateTime>(DateTime.Now);

        Console.Out.WriteLine(p1.Serialize());
        Console.Out.WriteLine(p2.Serialize());
        Console.Out.WriteLine(JsonConvert.SerializeObject(p3));
        Console.Out.WriteLine(JsonConvert.SerializeObject(p4));
    }

The Output will look something like this:

{"Int32":5}
{"String":"Message"}
{"Double":0.0}
{"DateTime":"2016-08-25T00:18:31.4882199-04:00"}

Upvotes: 0

Milton Hernandez
Milton Hernandez

Reputation: 664

Here's a potentially easier way to achieve it. All you need to do is to have Response extend JObject, like this:

public class Response<T>: Newtonsoft.Json.Linq.JObject
{
    private static string TypeName = (typeof(T)).Name;

    private T _data;

    public T Data {
        get { return _data; }
        set {
            _data = value;
            this[TypeName] = Newtonsoft.Json.Linq.JToken.FromObject(_data);   
        }
    }
}

If you do that, the following would work as you expect:

   static void Main(string[] args)
    {
        var p1 = new  Response<Int32>();
        p1.Data = 5;
        var p2 = new Response<string>();
        p2.Data = "Message";


        Console.Out.WriteLine("First: " + JsonConvert.SerializeObject(p1));
        Console.Out.WriteLine("Second: " + JsonConvert.SerializeObject(p2));
    }

Output:

First: {"Int32":5}
Second: {"String":"Message"}

In case you can't have Response<T> extend JObject, because you really need it to extend Response, you could have Response itself extend JObject, and then have Response<T> extend Response as before. It should work just the same.

Upvotes: 3

Thomas Levesque
Thomas Levesque

Reputation: 292615

What you're trying to do is possible, but not trivial, and can't be done with only the built-in attributes from JSON.NET. You'll need a custom attribute, and a custom contract resolver.

Here's the solution I came up with:

Declare this custom attribute:

[AttributeUsage(AttributeTargets.Property)]
class JsonPropertyGenericTypeNameAttribute : Attribute
{
    public int TypeParameterPosition { get; }

    public JsonPropertyGenericTypeNameAttribute(int position)
    {
        TypeParameterPosition = position;
    }
}

Apply it to your Data property

public class Response<T> : Response
{
    [JsonPropertyGenericTypeName(0)]
    public T Data { get; set; }
}

(0 is the position of T in Response<T>'s generic type parameters)

Declare the following contract resolver, which will look for the JsonPropertyGenericTypeName attribute and get the actual name of the type argument:

class GenericTypeNameContractResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var prop = base.CreateProperty(member, memberSerialization);
        var attr = member.GetCustomAttribute<JsonPropertyGenericTypeNameAttribute>();
        if (attr != null)
        {
            var type = member.DeclaringType;
            if (!type.IsGenericType)
                throw new InvalidOperationException($"{type} is not a generic type");
            if (type.IsGenericTypeDefinition)
                throw new InvalidOperationException($"{type} is a generic type definition, it must be a constructed generic type");
            var typeArgs = type.GetGenericArguments();
            if (attr.TypeParameterPosition >= typeArgs.Length)
                throw new ArgumentException($"Can't get type argument at position {attr.TypeParameterPosition}; {type} has only {typeArgs.Length} type arguments");
            prop.PropertyName = typeArgs[attr.TypeParameterPosition].Name;
        }
        return prop;
    }
}

Serialize with this resolver in your serialization settings:

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

This will give the following output for Response<Foo>

{
  "Foo": {
    "Id": 0,
    "Name": null
  }
}

Upvotes: 9

Related Questions