Nine times out of ten, a [JsonPropertyName] or [JsonIgnore] attribute that is “not working” belongs to a different JSON library than the one doing the serialising. .NET has two: System.Text.Json, built in, and Newtonsoft.Json, the older package. Their attributes have similar names, live in different namespaces, and each library ignores the other’s. Fix the namespace and the attribute starts working.
Two libraries, two attribute sets#
| Purpose | System.Text.Json | Newtonsoft.Json |
|---|---|---|
| Namespace | System.Text.Json.Serialization |
Newtonsoft.Json |
| Rename a property | [JsonPropertyName("x")] |
[JsonProperty("x")] |
| Skip a property | [JsonIgnore] |
[JsonIgnore] |
| Serialise | JsonSerializer.Serialize(obj) |
JsonConvert.SerializeObject(obj) |
Note that [JsonIgnore] exists in both, with the same name. That is the trap: your using line decides which one you get, and the compiler cannot warn you because both are valid.
Step 1: which serialiser is running?#
Look at the code that produces the JSON, not the model:
JsonSerializer.SerializeorJsonSerializer.Deserialize— System.Text.Json.JsonConvert.SerializeObjectorJObject— Newtonsoft.- ASP.NET Core controllers and minimal APIs — System.Text.Json by default, unless
AddNewtonsoftJson()is called inProgram.cs. HttpClient.GetFromJsonAsyncandPostAsJsonAsync— System.Text.Json.- Older libraries, Azure Functions v3, some SDKs — often Newtonsoft.
Then look at the top of the model file. If the serialiser is System.Text.Json and the file says using Newtonsoft.Json;, that is the bug.
using System.Text.Json.Serialization; // must match the serialiser in use
public class User
{
[JsonPropertyName("user_name")]
public string UserName { get; set; } = "";
[JsonIgnore]
public string PasswordHash { get; set; } = "";
}
Other causes, once the namespace is right#
The property is not public, or has no setter#
System.Text.Json serialises public properties only. A private property, a public field, or a property with only a getter is skipped regardless of attributes. For fields, opt in with [JsonInclude] or IncludeFields = true in the options. Read-only properties serialise but do not deserialise unless you use an init setter or a constructor.
The attribute is on an interface or base class, but the runtime type differs#
Attributes are read from the declared type being serialised. Serialising a List<object> loses the derived-type attributes; serialise the concrete type or use the polymorphism attributes.
Source generation is in use#
If the project uses JsonSerializerContext for AOT or performance, the attributes must be visible at compile time to the generated code. Adding an attribute and not rebuilding, or having the model in a project the context cannot see, silently reverts to defaults.
JsonIgnore with a condition#
System.Text.Json’s [JsonIgnore] takes a Condition. The default is Always, but WhenWritingNull and WhenWritingDefault mean the property still appears when it has a value — which looks like the attribute being ignored if you expected it always to be omitted.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Nickname { get; set; }
Global naming instead of attributes#
If every property needs renaming to snake_case or camelCase, do it once in the options rather than on each property:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
var json = JsonSerializer.Serialize(user, options);
// ASP.NET Core: apply to every response
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower);
A [JsonPropertyName] on an individual property overrides the policy, so the two combine cleanly.
Deserialising: names must match exactly#
By default System.Text.Json is case-sensitive when reading. JSON with "username" does not populate a property named UserName unless you set PropertyNameCaseInsensitive = true or add the attribute with the exact incoming name. ASP.NET Core sets case-insensitivity for you; console applications do not.
Pick one library#
Mixed projects are where this problem lives. Choose System.Text.Json for anything new; it is faster, built in and what the framework uses. Keep Newtonsoft only where a dependency requires it, and in those files be explicit:
using STJ = System.Text.Json.Serialization;
using NSJ = Newtonsoft.Json;
public class Legacy
{
[STJ.JsonPropertyName("id")]
[NSJ.JsonProperty("id")]
public int Id { get; set; }
}
Aliasing the namespaces makes the choice visible on every attribute and stops the wrong autocomplete from ever compiling silently.
Questions people ask#
Why does ASP.NET Core ignore my Newtonsoft attributes?
Because it uses System.Text.Json since .NET Core 3.0. Either switch the attributes or call AddNewtonsoftJson() to switch the serialiser back.
Does JsonPropertyName affect deserialisation too?
Yes. The name is used in both directions.
Can I rename a property for output only?
Not with a single attribute. Use a separate DTO for output, or a custom converter.
Which is faster?
System.Text.Json, particularly with source generation. Newtonsoft’s advantage is flexibility with unusual JSON shapes.
Where to go next#
- File I/O and serialisation in C# — the wider picture.
- ASP.NET Core error handling — shaping error responses as JSON.
- C# and ASP.NET Core — where the default serialiser is set.