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. StatusCodestatus_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")