Updated on

To retrieve JSON property names in C#, we read the name attribute on each property. In System.Text.Json that attribute is [JsonPropertyName], in Newtonsoft.Json it is [JsonProperty], and we can read either one through reflection or ask the serializer directly.

System.Text.Json is the serializer built into .NET and the one ASP.NET Core uses unless we change it (see the default JSON serializer in ASP.NET Core), so we start there. Newtonsoft.Json follows for the many projects that still run on it.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in!

Why Retrieve JSON Property Names?

The JSON format has become a de facto standard due to its readability and simplicity. Since it is a mapping of properties to data, JSON enables us to map .NET objects to JSON objects in a system-agnostic way. These JSON properties play a vital role during serialization and deserialization. Because of this we often require retrieving these properties when parsing JSON objects. Let’s see how we can accomplish this using System.Text.Json and Newtonsoft.Json.

How Do We Retrieve JSON Property Names With System.Text.Json?

System.Text.Json sets a custom JSON name with the [JsonPropertyName] attribute, and there are two ways to read that name back.

The first is reflection. We call GetProperties() on the type, ask each property for its JsonPropertyNameAttribute and fall back to the C# property name when the attribute is missing. That gives us exactly what we wrote in the code.

The second is to ask the serializer. JsonSerializerOptions.GetTypeInfo() returns the contract the serializer builds for a type, and every entry in its Properties list carries the final JSON name in Name. That name already reflects everything that can change it, including a naming policy such as camelCase.

For classes like ours, the two answers match until a naming policy or a [JsonIgnore] attribute enters the picture. Reflection reads the attribute and nothing else. If we need the names that actually end up in the JSON, the serializer is the one to ask.

If System.Text.Json is new to you, our introduction to System.Text.Json covers the basics before we go further.

Using System.Text.Json With Reflection

System.Text.Json uses the JsonPropertyName attribute to specify the property name:

using System.Text.Json.Serialization;

public class Sales
{
    [JsonPropertyName("Slx_PER_Year")]
    public string? YearlySales { get; set; }

    [JsonPropertyName("Slx_PER_DAY")]
    public string? DailySales { get; set; }
}

We are extracting properties using Reflection and LINQ, in a static JsonTextImplementation class:

using System.Reflection;
using System.Text.Json.Serialization;

public static string[] GetJsonPropertyNames(object obj)
{
    Type type = obj.GetType();
    PropertyInfo[] properties = type.GetProperties();
    string[] propertyNames = properties
        .Select(property =>
            property.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name ?? property.Name)
        .ToArray();

    return propertyNames;
}

The GetProperties() method returns a PropertyInfo object for every public property of the type. Then, using the LINQ Select(), we iterate over each of the properties, checking for the JsonPropertyNameAttribute. If the attribute is found, we return its value as the property name, otherwise we return the default property name.

Let’s see it in action:

var sales = new Sales 
{ 
    YearlySales = "365000", 
    DailySales = "1000" 
};

var propertyNames = JsonTextImplementation.GetJsonPropertyNames(sales);

Console.WriteLine("JSON property names:");
foreach (var propertyName in propertyNames)
{
    Console.WriteLine(propertyName);
}

Here we retrieve the property names of our sales object and print it out to the console:

JSON property names:
Slx_PER_Year
Slx_PER_DAY

Reading an attribute back through reflection works the same way for any attribute, including our own custom attributes. The catch is that reflection only sees what is written on the property. It doesn’t know which naming policy the serializer will apply, and it still lists a property marked [JsonIgnore].

Using JsonTypeInfo to Get the Names the Serializer Uses

Reflection tells us what the attribute says. The serializer can tell us what it will actually write, and since .NET 7 it exposes that through JsonTypeInfo.

Let’s add a second method to our JsonTextImplementation class:

using System.Text.Json;
using System.Text.Json.Serialization.Metadata;

public static string[] GetSerializedPropertyNames<T>(JsonSerializerOptions? options = null)
{
    options ??= JsonSerializerOptions.Default;
    options.MakeReadOnly(populateMissingResolver: true);

    JsonTypeInfo typeInfo = options.GetTypeInfo(typeof(T));

    return typeInfo.Properties
        .Where(property => property.Get is not null || property.Set is not null)
        .Select(property => property.Name)
        .ToArray();
}

Here, we first fall back to JsonSerializerOptions.Default when no options come in. Then we call MakeReadOnly(populateMissingResolver: true). A freshly created JsonSerializerOptions has no type info resolver until its first use, and GetTypeInfo() throws a NotSupportedException without one. This call plugs in the default reflection-based resolver and locks the options, which the serializer would do on its first call anyway.

Next, GetTypeInfo() gives us the contract for our type. Each item in Properties is a JsonPropertyInfo, and its Name is the JSON name after the attribute and the naming policy have both had their say.

Finally, we skip properties that have neither a getter nor a setter in the contract. That is how the contract marks a property with [JsonIgnore]: it stays in the list but the serializer never reads or writes it.

For our Sales class, nothing changes when we print the result with the same loop:

var propertyNames = JsonTextImplementation.GetSerializedPropertyNames<Sales>();
Slx_PER_Year
Slx_PER_DAY

The difference shows up with a class that has a naming policy and an ignored property in play:

using System.Text.Json.Serialization;

public class Customer
{
    [JsonPropertyName("FirstName")]
    public string? Name { get; set; }

    [JsonIgnore]
    public string? Password { get; set; }

    public int Age { get; set; }
}

Let’s ask both methods for its names, using the web defaults that turn on camelCase:

using System.Text.Json;

var webOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);

var fromReflection = JsonTextImplementation.GetJsonPropertyNames(new Customer());
var fromSerializer = JsonTextImplementation.GetSerializedPropertyNames<Customer>(webOptions);

Reflection returns FirstName, Password and Age. The serializer returns FirstName and age.

FirstName survives the camelCase policy because the attribute wins. Microsoft Learn says the name set by [JsonPropertyName] “takes precedence over property naming policies.” Age has no attribute, so the policy turns it into age. And Password is gone because the serializer will never write it.

Each list answers a different question. If we want to know what a developer wrote on the class, reflection is enough. If we want the keys a client will see in the response, we ask the serializer. For more on how policies rename properties, see our articles on camelCase serialization with System.Text.Json and on writing a custom naming policy.

The default resolver relies on reflection. In an app published with trimming, which Native AOT always uses, reflection-based serialization is off by default, and GetTypeInfo() throws unless the options carry a source-generated JsonSerializerContext that knows the type.

How Do We Retrieve JSON Property Names With Newtonsoft.Json?

Newtonsoft.Json has its own attribute for the same job, [JsonProperty]. The two libraries ignore each other’s attributes, so a class serialized with Json.NET needs [JsonProperty], and reading [JsonPropertyName] from it tells us nothing about its JSON. That is why our sample’s Sales class carries both.

We’ll look at three ways to get the names out of Json.NET.

Reflection reads JsonPropertyAttribute.PropertyName from each property and falls back to the C# name, the same pattern we used with System.Text.Json.

A serialization round trip turns the object into JSON, reads it back into a dictionary and returns the keys. It needs a real instance and does the most work.

A contract resolver asks Json.NET for the JsonObjectContract it builds for the type and looks a property up by its JSON name. Like GetTypeInfo(), it reports names after any naming strategy has run.

To get started we need to add the NuGet package to our project:

dotnet add package Newtonsoft.Json

Using Newtonsoft.Json With Reflection

Here we are using the JsonProperty attribute from Newtonsoft.Json to add custom property names to our object properties. We can then retrieve those custom attributes via reflection. The JsonProperty attribute is often used to aid in mapping JSON properties into .NET objects. By default the property names themselves are used for serialization and deserialization. Sometimes the property names do not align perfectly with our standard naming conventions. In these situations we can employ the JsonProperty attribute to map between the two.

Let’s take a look at an example (in our sample, these attributes sit on the same Sales class as the [JsonPropertyName] ones): 

using Newtonsoft.Json;

public class Sales
{
    [JsonProperty("Slx_PER_Year")]
    public string? YearlySales { get; set; }

    [JsonProperty("Slx_PER_DAY")]
    public string? DailySales { get; set; }
}

In this example, we see that the JSON property names do not align with standard .NET property naming conventions. By using the JsonProperty attribute we preserve proper serialization and deserialization behavior whilst maintaining the naming convention in our code as per standards.

Now, let’s define a method in a static JsonHelper class to retrieve JSON property names via reflection:

using System.Reflection;
using Newtonsoft.Json;

public static IEnumerable<string> RetrievalUsingReflection(object obj)
{
    foreach (var property in obj.GetType().GetProperties())
    {
        var jsonProperty = property.GetCustomAttribute<JsonPropertyAttribute>();
        var jsonPropertyName = jsonProperty?.PropertyName; 

        yield return jsonPropertyName != null 
            ? jsonPropertyName 
            : property.Name;
    }
}

While iterating over the properties of the type of our obj we check for JsonPropertyAttribute and extract them. For each one found, we return the PropertyName value, otherwise we return the property name.

Let’s see how it works:

var sales = new Sales
{
    YearlySales = "365000",
    DailySales = "1000"
};

var propertyNames = JsonHelper.RetrievalUsingReflection(sales);

foreach (var propertyName in propertyNames)
{
    Console.WriteLine(propertyName);
}

We are using our sales instance to retrieve the JSON property names with our previously defined RetrievalUsingReflection() method and print the output of the result to the console:

Slx_PER_Year
Slx_PER_DAY

Using Newtonsoft.Json With Serialization

For this approach, we are using JsonConvert.DeserializeObject() method of Newtonsoft.Json library to retrieve the JSON property name:

public static IEnumerable<string> RetrievalUsingSerialization(object obj)
{
    string json = JsonConvert.SerializeObject(obj);

    var values = JsonConvert.DeserializeObject<Dictionary<string, object?>>(json);
        
    return values != null 
        ? values.Keys 
        : Enumerable.Empty<string>();
}

We keep this implementation simple and serialize our obj to a string with JsonConvert.SerializeObject(). We then deserialize the JSON string to a Dictionary<string, object?> with JsonConvert.DeserializeObject(). Using the dictionary Keys property we can now retrieve our property names.

Let’s repeat our example from the previous section using our new method:

var sales = new Sales
{
    YearlySales = "365000",
    DailySales = "1000"
};

var propertyNames = JsonHelper.RetrievalUsingSerialization(sales);

foreach (var propertyName in propertyNames)
{
    Console.WriteLine(propertyName);
}

Similarly, we call RetrievalUsingSerialization() on our sales object and observe the following output:

Slx_PER_Year
Slx_PER_DAY

Going through a dictionary is also how we serialize a dictionary to JSON in the first place, if we ever need the reverse trip.

Using Newtonsoft.Json With Contract Resolver

Here we are using IContractResolver from Newtonsoft.Json to retrieve the JSON property name.

Let’s see how we can do this:

using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public static class ResolverHelper
{
    static readonly IContractResolver _defaultResolver = JsonSerializer.CreateDefault().ContractResolver;

    public static JsonProperty GetJsonProperty<T>(T obj, string jsonName, bool exact = false, 
        IContractResolver? resolver = null)
    {
        ArgumentNullException.ThrowIfNull(obj);

        resolver ??= _defaultResolver;
        var objType = typeof(T);

        if (resolver.ResolveContract(objType) is not JsonObjectContract contract)
            throw new ArgumentException($"{objType} is not serialized as a JSON object");

        var property = contract.Properties.GetProperty(jsonName, exact 
            ? StringComparison.Ordinal 
            : StringComparison.OrdinalIgnoreCase);

        if (property == null)
            throw new ArgumentException($"Property {jsonName} was not found.");

        return property;
    }
}

With the IContractResolver we pass as resolver as a parameter, we get the contract definition of our obj instance. If there is no resolver passed on, we take the default resolver implementation from _defaultResolver which we fill from the JsonSerializer class via the CreateDefault() method. After performing various checks, it retrieves the JsonProperty based on the specified property name while also considering case sensitivity as determined by the exact parameter via the contract we resolved. 

Let’s see it in action:

using Newtonsoft.Json.Serialization;

var sales = new Sales
{
    YearlySales = "365000",
    DailySales = "1000"
};

JsonProperty salesPerYearProperty = ResolverHelper.GetJsonProperty(sales, "Slx_per_Year");
JsonProperty salesPerDayProperty = ResolverHelper.GetJsonProperty(sales, "Slx_per_DAY");

Console.WriteLine($"{salesPerYearProperty.PropertyName}");
Console.WriteLine($"{salesPerDayProperty.PropertyName}");

The output of this for our sales object will be:

Slx_PER_Year
Slx_PER_DAY

Which Way of Retrieving JSON Property Names Should We Use?

We have five ways now, and they differ in what they actually read:

ApproachLibraryWhat it readsApplies a naming policyLeaves out JsonIgnore properties
Reflection on JsonPropertyNameSystem.Text.JsonThe attribute on each propertyNoNo
GetTypeInfo() metadataSystem.Text.JsonThe serializer's contractYesYes, with the getter and setter check
Reflection on JsonPropertyNewtonsoft.JsonThe attribute on each propertyNoNo
Serialize, then read dictionary keysNewtonsoft.JsonThe JSON the serializer writesIf JsonConvert.DefaultSettings or an attribute sets oneYes
Contract resolverNewtonsoft.JsonJson.NET's contractYes, from the resolver or an attributeNo, it flags them with Ignored

For a quick look at the names written on a class, reflection is the simplest option and works for both libraries. When the names have to match the JSON a client receives, ask the serializer we actually use: GetTypeInfo() for System.Text.Json and the contract resolver for Newtonsoft.Json. The serialization round trip gives the same answer as the serializer but needs an instance and builds a whole JSON string to get it, so we’d keep it for the cases where we already have the JSON in hand.

Conclusion

In this article, we explored different ways to retrieve JSON property names in C#. We started with System.Text.Json, reading the JsonPropertyName attribute through reflection and asking the serializer for its contract with GetTypeInfo(). Then we covered the three Newtonsoft.Json approaches: reflection, a serialization round trip and the contract resolver.

Reflection is the simplest way to see what a class declares. When naming policies or ignored properties are in play, we should read the names from the serializer’s own contract.

Tested with .NET 10 (SDK 10.0.302), Newtonsoft.Json 13.0.4 and NUnit 5.0.0.