Newtonsoft JSON dynamic property name

31,887

Solution 1

You can do this with a custom ContractResolver. The resolver can look for a custom attribute which will signal that you want the name of the JSON property to be based on the class of the items in the enumerable. If the item class has another attribute on it specifying its plural name, that name will then be used for the enumerable property, otherwise the item class name itself will be pluralized and used as the enumerable property name. Below is the code you would need.

First let's define some custom attributes:

public class JsonPropertyNameBasedOnItemClassAttribute : Attribute
{
}

public class JsonPluralNameAttribute : Attribute
{
    public string PluralName { get; set; }
    public JsonPluralNameAttribute(string pluralName)
    {
        PluralName = pluralName;
    }
}

And then the resolver:

public class CustomResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty prop = base.CreateProperty(member, memberSerialization);
        if (prop.PropertyType.IsGenericType && member.GetCustomAttribute<JsonPropertyNameBasedOnItemClassAttribute>() != null)
        {
            Type itemType = prop.PropertyType.GetGenericArguments().First();
            JsonPluralNameAttribute att = itemType.GetCustomAttribute<JsonPluralNameAttribute>();
            prop.PropertyName = att != null ? att.PluralName : Pluralize(itemType.Name);
        }
        return prop;
    }

    protected string Pluralize(string name)
    {
        if (name.EndsWith("y") && !name.EndsWith("ay") && !name.EndsWith("ey") && !name.EndsWith("oy") && !name.EndsWith("uy"))
            return name.Substring(0, name.Length - 1) + "ies";

        if (name.EndsWith("s"))
            return name + "es";

        return name + "s";
    }
}

Now you can decorate the variably-named property in your PagedData<T> class with the [JsonPropertyNameBasedOnItemClass] attribute:

public class PagedData<T>
{
    [JsonPropertyNameBasedOnItemClass]
    public IEnumerable<T> Data { get; private set; }
    ...
}

And decorate your DTO classes with the [JsonPluralName] attribute:

[JsonPluralName("Users")]
public class UserDTO
{
    ...
}

[JsonPluralName("Items")]
public class ItemDTO
{
    ...
}

Finally, to serialize, create an instance of JsonSerializerSettings, set the ContractResolver property, and pass the settings to JsonConvert.SerializeObject like so:

JsonSerializerSettings settings = new JsonSerializerSettings
{
    ContractResolver = new CustomResolver()
};

string json = JsonConvert.SerializeObject(pagedData, settings);

Fiddle: https://dotnetfiddle.net/GqKBnx

If you're using Web API (looks like you are), then you can install the custom resolver into the pipeline via the Register method of the WebApiConfig class (in the App_Start folder).

JsonSerializerSettings settings = config.Formatters.JsonFormatter.SerializerSettings;
settings.ContractResolver = new CustomResolver();

Another Approach

Another possible approach uses a custom JsonConverter to handle the serialization of the PagedData class specifically instead using the more general "resolver + attributes" approach presented above. The converter approach requires that there be another property on your PagedData class which specifies the JSON name to use for the enumerable Data property. You could either pass this name in the PagedData constructor or set it separately, as long as you do it before serialization time. The converter will look for that name and use it when writing out JSON for the enumerable property.

Here is the code for the converter:

public class PagedDataConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(PagedData<>);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Type type = value.GetType();

        var bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
        string dataPropertyName = (string)type.GetProperty("DataPropertyName", bindingFlags).GetValue(value);
        if (string.IsNullOrEmpty(dataPropertyName)) 
        {
            dataPropertyName = "Data";
        }

        JObject jo = new JObject();
        jo.Add(dataPropertyName, JArray.FromObject(type.GetProperty("Data").GetValue(value)));
        foreach (PropertyInfo prop in type.GetProperties().Where(p => !p.Name.StartsWith("Data")))
        {
            jo.Add(prop.Name, new JValue(prop.GetValue(value)));
        }
        jo.WriteTo(writer);
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

To use this converter, first add a string property called DataPropertyName to your PagedData class (it can be private if you like), then add a [JsonConverter] attribute to the class to tie it to the converter:

[JsonConverter(typeof(PagedDataConverter))]
public class PagedData<T>
{
    private string DataPropertyName { get; set; }
    public IEnumerable<T> Data { get; private set; }
    ...
}

And that's it. As long as you've set the DataPropertyName property, it will be picked up by the converter on serialization.

Fiddle: https://dotnetfiddle.net/8E8fEE

Solution 2

UPD Sep 2020: @RyanHarlich pointed that proposed solution doesn't work out of the box. I found that Newtonsoft.Json doesn't initialize getter-only properties in newer versions, but I'm pretty sure it did ATM I wrote this answer in 2016 (no proofs, sorry :).

A quick-n-dirty solution is to add public setters to all properties ( example in dotnetfiddle ). I encourage you to find a better solution that keeps read-only interface for data objects. I haven't used .Net for 3 years, so cannot give you that solution myself, sorry :/


Another option with no need to play with json formatters or use string replacements - only inheritance and overriding (still not very nice solution, imo):

public class MyUser { }
public class MyItem { }

// you cannot use it out of the box, because it's abstract,
// i.e. only for what's intended [=implemented].
public abstract class PaginatedData<T>
{
    // abstract, so you don't forget to override it in ancestors
    public abstract IEnumerable<T> Data { get; }
    public int Count { get; }
    public int CurrentPage { get; }
    public int Offset { get; }
    public int RowsPerPage { get; }
    public int? PreviousPage { get; }
    public int? NextPage { get; }
}

// you specify class explicitly
// name is clear,.. still not clearer than PaginatedData<MyUser> though
public sealed class PaginatedUsers : PaginatedData<MyUser>
{
    // explicit mapping - more agile than implicit name convension
    [JsonProperty("Users")]
    public override IEnumerable<MyUser> Data { get; }
}

public sealed class PaginatedItems : PaginatedData<MyItem>
{
    [JsonProperty("Items")]
    public override IEnumerable<MyItem> Data { get; }
}

Solution 3

Here is a solution that doesn't require any change in the way you use the Json serializer. In fact, it should also work with other serializers. It uses the cool DynamicObject class.

The usage is just like you wanted:

var usersPagedData = new PagedData<User>("Users");
....

public class PagedData<T> : DynamicObject
{
    private string _name;

    public PagedData(string name)
    {
        if (name == null)
            throw new ArgumentNullException(nameof(name));

        _name = name;
    }

    public IEnumerable<T> Data { get; private set; }
    public int Count { get; private set; }
    public int CurrentPage { get; private set; }
    public int Offset { get; private set; }
    public int RowsPerPage { get; private set; }
    public int? PreviousPage { get; private set; }
    public int? NextPage { get; private set; }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        yield return _name;
        foreach (var prop in GetType().GetProperties().Where(p => p.CanRead && p.GetIndexParameters().Length == 0 && p.Name != nameof(Data)))
        {
            yield return prop.Name;
        }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (binder.Name == _name)
        {
            result = Data;
            return true;
        }

        return base.TryGetMember(binder, out result);
    }
}
Share:
31,887

Related videos on Youtube

Robert
Author by

Robert

I am a .NET developer, focused mainly on a web development. I know a bit of PHP, but my point of interest are .NET and Web technologies.

Updated on October 18, 2021

Comments

  • Robert
    Robert over 2 years

    Is there a way to change name of Data property during serialization, so I can reuse this class in my WEB Api.

    For an example, if i am returning paged list of users, Data property should be serialized as "users", if i'm returning list of items, should be called "items", etc.

    Is something like this possible:

    public class PagedData
    {
        [JsonProperty(PropertyName = "Set from constructor")]??
        public IEnumerable<T> Data { get; private set; }
        public int Count { get; private set; }
        public int CurrentPage { get; private set; }
        public int Offset { get; private set; }
        public int RowsPerPage { get; private set; }
        public int? PreviousPage { get; private set; }
        public int? NextPage { get; private set; }
    }
    

    EDIT:

    I would like to have a control over this functionality, such as passing name to be used if possible. If my class is called UserDTO, I still want serialized property to be called Users, not UserDTOs.

    Example

    var usersPagedData = new PagedData("Users", params...);
    
  • Robert
    Robert almost 8 years
    this looks very dirty. It would be easier to create a base class for paging, and inherit it in another class, and just name property as needed there.
  • Thomas Voß
    Thomas Voß almost 8 years
    A different solution and better one is to set an attribute to the Property you want to transmit as data. [JsonProperty(PropertyName = "Data")] public IEnumerable<T> Users { get; set; } Sorry, you already have this in your post... my bad...
  • Thomas Voß
    Thomas Voß almost 8 years
    One possibility then is to use Reflections.Emit I haven't used it myself but it gives you the possibility to generate classes, properties and code on the fly. There should be a possibility to inherit your type and to add the Attribute to it.
  • Robert
    Robert almost 8 years
    this is one of the solutions I had in mind, but laziness prevents me to create many classes :D. But it's elegant, and I have a full control over naming.
  • Robert
    Robert almost 8 years
    Brian, thank you for the detailed answer. Your solution is awesome, but it is not exactly what need. I still have the problem with lack of control. If I had a class which was named UserDTO, i will have a property named UserDTOs, and I simply wish to name it "users" without need to rename entire class.
  • Brian Rogers
    Brian Rogers almost 8 years
    @Robert With just a little more code, we can extend the resolver to allow you to specify the plural names of the DTO classes. I've updated my answer.
  • Brian Rogers
    Brian Rogers almost 8 years
    @Robert I added an alternative approach using a JsonConverter instead. Might be a little closer to what you originally had in mind.
  • pkuderov
    pkuderov almost 8 years
    I think DataPropertyName might (and should) be non-public.
  • Brian Rogers
    Brian Rogers almost 8 years
    @pkuderov Agreed; now the converter will allow this. Updated fiddle as well to illustrate.
  • Simon Mourier
    Simon Mourier almost 8 years
    Note there is now a pluralization service in .NET 4.5+: jamesmccaffrey.wordpress.com/2015/03/14/…
  • Robert
    Robert almost 8 years
    @BrianRogers this is awesome. Thank you very much for the effort. I will now be able to inject name via constructor, which is exactly method I needed.
  • Brian Rogers
    Brian Rogers almost 8 years
    @SimonMourier Thanks for the tip; I did not know this service existed.
  • Brian Rogers
    Brian Rogers almost 8 years
    @Robert No problem; Glad I was able to help.
  • Robert
    Robert almost 8 years
    @BrianRogers I will be able to reward with a bounty in 6 hours.
  • Brian Rogers
    Brian Rogers almost 8 years
    @Robert Thanks, I appreciate it!
  • Robert
    Robert almost 8 years
    Sorry for the late reply Simon. This is very elegant and nice solution. I wish i could reward you too with reputation.
  • Hunt
    Hunt over 5 years
    @BrianRogers i loved the first approach +1
  • Ryan Harlich
    Ryan Harlich over 3 years
    Hi Pkuderov, I tried this implementation with the abstract class in a library and the sealed class in a client and I am not having the property name changed in my JSON output.
  • pkuderov
    pkuderov over 3 years
    @RyanHarlich Hi, Ryan! It seems that Newtonsoft.Json doesn't initialize getter-only properties in newer versions. A dirty solution is to add public setters to all props: dotnetfiddle.net/IsI9IF. Sorry, I haven't used .Net for a couple of years, so I cannot give you quickly a more appropriate solution :)
  • Ryan Harlich
    Ryan Harlich over 3 years
    Hi Pkuderov, this solution does not work. I looked at the sample you sent and this line "var paginated_users = JsonConvert.DeserializeObject<PaginatedUsers>(json_str);" does not use polymorphism. It simple only uses a subclass handler instead of a base class handler with a subclass assigned to that handler. If you replace "var" with the base class I can guarantee this won't work; however, I am not going to try because I already tested with a library DLL and client and it does not work, so I would assume the same result would happen.
  • pkuderov
    pkuderov over 3 years
    Now I don't understand your problem. What doesn't mean it doesn't use polymorphism? The type of paginated_users is PaginatedUsers and it should be, because that's exactly what I tell to JsonConverter. It's also a subclass of PaginatedUsers<MyUser>. Did you try what you said (replace "var" with PaginatedUsers<MyUser>) right in provided fiddle?
  • Eight-Bit Guru
    Eight-Bit Guru almost 3 years
    Fantastic answer, did exactly what I needed.
  • jpt
    jpt over 2 years
    OP specifically said JSON.Net was the library in use. This solution excludes it.