Type Configuration
AspNetConventions lets you describe how each type should serialize using strongly-typed expressions, instead of decorating models with attributes. Per-type rules, global ignores, and reusable configuration classes all live behind one fluent API.
Per-Type Property Configuration
Use options.Json.ConfigureTypes to apply fine-grained rules to specific types using strongly-typed expressions.
options.Json.ConfigureTypes = cfg =>
{
cfg.Type<User>(type =>
{
type.Property(x => x.Id).Order(0);
type.Property(x => x.UserName).Name("username").Order(1);
type.Property(x => x.Password).Ignore();
type.Property(x => x.MiddleName).Ignore(IgnoreCondition.WhenWritingNull);
});
};
Renaming Properties
Override the serialized name of a specific property. The explicit name takes full precedence over the global CaseStyle:
cfg.Type<Product>(type =>
{
type.Property(x => x.InternalSku).Name("sku");
type.Property(x => x.DisplayName).Name("name");
type.Property(x => x.ListPriceCents).Name("price");
});
Result:
{ "sku": "ABC-001", "name": "Widget Pro", "price": 2999 }
Ordering Properties
Control the order in which properties appear in the serialized output. Lower values are written first:
cfg.Type<UserResponse>(type =>
{
type.Property(x => x.Id).Order(0);
type.Property(x => x.Name).Order(1);
type.Property(x => x.Email).Order(2);
type.Property(x => x.CreatedAt).Order(3);
});
Best Practice: Order Every Property
This behaviour is inherited from System.Text.Json / SystemTextJsonAdapter (default adapter).
For deterministic, predictable output, assign an explicit Order() to every property:
| Scenario | Recommendation |
|---|---|
| Full control over property order | Assign explicit order to every property |
| Pin one property to the top | Use Order(-1) for that property |
| Pin one property to the bottom | Use Order(int.MaxValue) |
| Unordered, rely on declaration order | Avoid mixing ordered and unordered properties |
Ignoring Properties
Exclude a property from serialization using an IgnoreCondition:
cfg.Type<User>(type =>
{
// Never serialized
type.Property(x => x.Password).Ignore();
// Omitted when null
type.Property(x => x.MiddleName).Ignore(IgnoreCondition.WhenWritingNull);
// Omitted when equal to the type's default (0, false, null)
type.Property(x => x.LoginAttempts).Ignore(IgnoreCondition.WhenWritingDefault);
});
Ignore() without arguments defaults to IgnoreCondition.Always.
Chaining Rules
All IJsonPropertyRuleBuilder methods return this and can be chained in any combination:
cfg.Type<OrderSummary>(type =>
{
type.Property(x => x.OrderId).Name("id").Order(0);
type.Property(x => x.CustomerName).Order(1);
type.Property(x => x.TotalAmount).Name("total").Order(2);
type.Property(x => x.Notes).Order(3).Ignore(IgnoreCondition.WhenWritingNull);
type.Property(x => x.InternalReference).Ignore();
});
Open Generic Type Configuration
Rules for open generic types (e.g. MyApiResponse<T>) apply to every closed variant at runtime. Pass a closed instantiation as the template for expression-based property selection:
options.Json.ConfigureTypes = cfg =>
{
// Rules apply to MyApiResponse<string>, MyApiResponse<User>, MyApiResponse<anything>
cfg.OpenGenericType<MyApiResponse<object>>(type =>
{
type.Property(x => x.Data).Order(3);
type.Property(x => x.InternalToken).Ignore();
});
};
The expression is resolved against the template type; the rule is stored under the open generic definition (MyApiResponse<>) and matched against all closed variants at serialization time.
Global Ignore Rules
When you want to suppress a property type or name across all types, use the global ignore methods instead of repeating cfg.Type<T>() for every affected type.
Ignore by Type
Suppress every property whose value type is (or inherits from) T:
options.Json.ConfigureTypes = cfg =>
{
// Hides every property of type Metadata (or any subclass) across all response types
cfg.IgnoreType<Metadata>();
// Hides nullable or non-nullable DateTimeOffset properties everywhere
cfg.IgnoreType<DateTimeOffset>(IgnoreCondition.WhenWritingDefault);
};
IgnoreType<T>() has the highest priority — it overrides any per-type per-property rule for the same property.
Ignore by Property Name
Suppress any property whose name matches a given string, regardless of which type it belongs to:
options.Json.ConfigureTypes = cfg =>
{
// Hides "statusCode" / "StatusCode" on every type that has it
cfg.IgnorePropertyName("StatusCode");
// Hides "internalRef" everywhere, only when null
cfg.IgnorePropertyName("internalRef", IgnoreCondition.WhenWritingNull);
};
The match is case-insensitive and tries the CLR name first, then the JSON-transformed name (e.g. StatusCode → status_code). A more specific per-type per-property rule takes precedence over this global rule.
Class-Based Configuration
For applications with many types to configure, inline delegates in ConfigureTypes can grow unwieldy. AspNetConventions supports a class-based approach using JsonTypeConfiguration<T>:
public class UserConfiguration : JsonTypeConfiguration<User>
{
public override void Configure(IJsonTypeRuleBuilder<User> rule)
{
rule.Property(x => x.Id).Order(0);
rule.Property(x => x.UserName).Order(1);
rule.Property(x => x.Password).Ignore();
rule.Property(x => x.RefreshToken).Ignore();
}
}
public class OrderConfiguration : JsonTypeConfiguration<Order>
{
public override void Configure(IJsonTypeRuleBuilder<Order> rule)
{
rule.Property(x => x.OrderId).Name("id").Order(0);
rule.Property(x => x.InternalReference).Ignore();
rule.Property(x => x.Notes).Ignore(IgnoreCondition.WhenWritingNull);
}
}
Register configurations individually via ConfigureTypes:
options.Json.ConfigureTypes = cfg =>
{
cfg.Type<User>(type => new UserConfiguration().Configure(type));
};
Or, preferably, register them all at once via assembly scanning.
Assembly Scanning
ScanAssemblies discovers all non-abstract, non-generic subclasses of JsonTypeConfigurationBase (which includes both JsonTypeConfiguration<T> and JsonOpenGenericTypeConfiguration<T>) and registers their rules automatically:
options.Json.ScanAssemblies(typeof(UserConfiguration).Assembly);
// Multiple assemblies
options.Json.ScanAssemblies(
typeof(UserConfiguration).Assembly,
typeof(ExternalTypeConfiguration).Assembly
);
No manual registration needed. Add a new JsonTypeConfiguration<T> class in a scanned assembly and it is picked up at the next startup.
Open Generic Type Configuration (Class-Based)
Use JsonOpenGenericTypeConfiguration<T> to define class-based rules for open generic types:
// T must be a closed generic — used only as a property-selection template
public class MyApiResponseConfiguration : JsonOpenGenericTypeConfiguration<MyApiResponse<object>>
{
public override void Configure(IJsonTypeRuleBuilder<MyApiResponse<object>> rule)
{
rule.Property(x => x.Data).Order(3);
rule.Property(x => x.TraceId).Ignore();
}
}
Rules are stored under MyApiResponse<> (the open generic definition) and matched against MyApiResponse<User>, MyApiResponse<Order>, etc. at runtime.
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") |