JSON Serialization
AspNetConventions provides a unified JSON configuration layer that applies consistently across your entire ASP.NET Core application — API responses, model serialization, and everything in between.
Why JSON Serialization?
ASP.NET Core’s JSON configuration is scattered: casing policies live on JsonSerializerOptions, per-property behavior requires [JsonIgnore] or custom converters, and property ordering is controlled attribute-by-attribute. There’s no single, coherent place to describe how your types should serialize.
Without AspNetConventions:
// Program.cs
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.WriteIndented = true;
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
});
// Property behavior scattered across model classes
// No central control over serialization behavior
public class User
{
[JsonPropertyOrder(0)]
public int Id { get; set; }
[JsonIgnore]
public string? Password { get; set; }
[JsonPropertyName("user_name")]
public string UserName { get; set; }
}
With AspNetConventions:
builder.Services.AddControllers()
.AddAspNetConventions(options =>
{
options.Json.WriteIndented = true;
options.Json.CaseStyle = CasingStyle.SnakeCase;
options.Json.ConfigureTypes = cfg =>
{
cfg.Type<User>(type =>
{
type.Property(x => x.Id).Order(0);
type.Property(x => x.Password).Ignore();
type.Property(x => x.UserName).Name("user_name");
});
};
});
No attributes on your models. No scattered options. One place.
Features
- Global casing style — Apply camelCase, snake_case, kebab-case, or PascalCase to all JSON property names application-wide
- Per-type property rules — Rename, reorder, or ignore specific properties on specific types using strongly-typed expressions
- Open generic type support — Define rules once for e.g.
MyClass<T>, apply them to all closed variants at runtime - Class-based configuration — Separate type configurations into dedicated classes, keeping
Program.csclean - Assembly scanning — Auto-discover and register all configuration classes in one or more assemblies.
- Global ignore rules — Suppress a property type (
IgnoreType<T>) or a property name (IgnorePropertyName) across all types at once - Zero per-request overhead — All rules are compiled into a snapshot at startup.
- Pluggable adapter — Swap the underlying serializer via
ConfigureAdapter<TAdapter, TOptions>
Before & After
Without AspNetConventions:
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
});
With AspNetConventions:
builder.Services.AddControllers()
.AddAspNetConventions(options =>
{
options.Json.CaseStyle = CasingStyle.SnakeCase;
});
Without AspNetConventions:
using System.Text.Json;
public class OrderSummary
{
[JsonPropertyName("id")]
[JsonPropertyOrder(0)]
public Guid OrderId { get; set; }
[JsonPropertyOrder(1)]
public string CustomerName { get; set; }
[JsonIgnore]
public string InternalReference { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Notes { get; set; }
}
With AspNetConventions:
cfg.Type<OrderSummary>(type =>
{
type.Property(x => x.OrderId).Name("id").Order(0);
type.Property(x => x.CustomerName).Order(1);
type.Property(x => x.InternalReference).Ignore();
type.Property(x => x.Notes).Ignore(IgnoreCondition.WhenWritingNull);
});
Your model class stays clean.
Inline (Program.cs):
options.Json.ConfigureTypes = cfg =>
{
cfg.Type<User>(...);
cfg.Type<Order>(...);
cfg.Type<Product>(...);
// grows without bound
};
Class-based (scales cleanly):
public class UserConfiguration : JsonTypeConfiguration<User>
{
public override void Configure(IJsonTypeRuleBuilder<User> rule)
{
rule.Property(x => x.Id).Order(0);
rule.Property(x => x.Password).Ignore();
}
}
// Register individually or via assembly scanning
options.Json.ScanAssemblies(typeof(UserConfiguration).Assembly);
Startup Flow
All configuration happens once at startup — there’s no per-request overhead:
Startup
│
├─ ConfigureTypes delegate runs → rules collected
│
├─ ScanAssemblies → discovers JsonTypeConfiguration<T> subclasses → rules collected
│
├─ JsonTypesConfigurationBuilder.CreateSnapshot() → immutable JsonTypeRulesSnapshot
│
└─ JsonTypeInfoResolver(snapshot) registered as DefaultJsonTypeInfoResolver
└─ Zero per-request cost
Resolution Priority
When multiple rules could apply to the same property, AspNetConventions resolves them in this order (highest wins):
| Priority | Rule source | API |
|---|---|---|
| 1 (highest) | Type-level ignore | cfg.IgnoreType<T>() |
| 2 | Per-type per-property rule | cfg.Type<T>(t => t.Property(x => x.Prop)...) |
| 3 (lowest) | Global property-name ignore | cfg.IgnorePropertyName("name") |