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

4

14

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; }
}
Hypoblast answered 24/8, 2016 at 16:31 Comment(5)
attributes require constant values.Outfall
@DanielA.White more precisely, constant values.Flosser
This can't work; attributes need constant values, and your PropName property is only evaluated at runtimeAgio
i wonder if theres a pure json.net solution here or some extension in its flexiblility.Outfall
Related: Json.NET getting generic property types name on serialization?.Tarter
A
9

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
  }
}
Agio answered 24/8, 2016 at 17:1 Comment(1)
Great answer! Works also for having List<T> inside MyClass<T>... In my case I did not want to be dependent on the the name of T (in case someone decides to rename it), but rather used constant field in T: prop.PropertyName = typeArgs[0].GetField("nameInJson").GetRawConstantValue().ToString();Streamlet
P
3

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.

Palladium answered 24/8, 2016 at 21:5 Comment(2)
That's a creative solution, but "All you need to do is to have Response extend JObject" is a pretty big requirement...Agio
Well, if Response can't be extended, he can use composition and create a similar Container<T> class that would be a property of Response<T>. I'll try to add an example later...Palladium
P
0

@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"}
Palladium answered 25/8, 2016 at 4:19 Comment(1)
what if i only have 1 unknown parameter and others that are known? such as: public class ReturnResponse<T> { [JsonProperty("status")] public Result status { get; set; } private static string TypeName = (typeof(T)).Name; private T _data; public T Data { get { return _data; } set { _data = value; this[TypeName] = JToken.FromObject(_data); } } } - only returns the generic data and ignores the status parameterEmelda
C
0

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

Couching answered 20/8 at 15:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.