Problem
#[gts_type_schema] / struct_to_gts_schema injects #[derive(schemars::JsonSchema)] from inside the attribute-macro expansion. That has two downstream effects that together make it impossible to attach standard schemars validation helpers — #[schemars(length(min = 1, max = 255))], #[schemars(email)], #[schemars(extend(\"format\" = \"uuid\"))] — to struct fields.
1. Helper-attribute ordering: legacy_derive_helpers deny
#[schemars(...)] is a derive helper attribute: the parser only recognises it as legal once a corresponding #[derive(JsonSchema)] has been registered on the same item. Because gts_type_schema injects the derive at expansion time, at first parse the helpers look orphaned and rustc fires the legacy_derive_helpers lint (deny-by-default in 2024 edition). The user cannot use any schemars helper on a GTS struct's fields.
2. Workaround "add the derive manually" doesn't work — duplicate impl
The obvious workaround is to put #[derive(schemars::JsonSchema)] on the struct yourself, above #[gts_type_schema(...)]. gts-macros already contains a has_derive check (src/lib.rs:351) and add_missing_derives filters by it (src/lib.rs:481-513), so in principle this should suppress the auto-injection. In practice on 0.9.3 we still get duplicate impl JsonSchema for X → error[E0119]: conflicting implementations of trait.
Reproducer (boiled down from account-management-sdk::gts::UserV1):
```rust
use modkit_gts::gts_type_schema; // wraps gts_macros::struct_to_gts_schema
#[derive(schemars::JsonSchema)] // try to claim the derive up-front
#[gts_type_schema(
dir_path = "schemas",
schema_id = "gts.cf.core.am.user.v1~",
description = "...",
properties = "id,username",
base = true,
)]
pub struct UserV1 {
pub id: gts::GtsInstanceId,
#[schemars(length(min = 1, max = 255))]
pub username: String,
}
```
Observed: E0119 on JsonSchema for UserV1. Expected (per the in-code comment on has_derive): macro detects the user derive and skips its own.
Why it matters
Real-world contract drift: hand-authored JSON Schemas in docs/schemas/ declare minLength, maxLength, format: email on string fields. The Rust-generated mirror via #[gts_type_schema] produces a schema without those constraints, so runtime validators accept any non-empty string for length-constrained fields and any string for email-formatted fields. Same shape will repeat for every future GTS base envelope that wants to express even basic field constraints. This is tracked in our codebase as TODO(am-user-schema-constraints).
Proposal
Two parts, in priority order:
- Fix the
has_derive suppression so a user-supplied #[derive(schemars::JsonSchema)] (and likewise Serialize/Deserialize for base structs) reliably prevents the macro's own injection. May require either (a) examining attrs after derive expansion, or (b) reordering so the macro's add_missing_derives runs against the attrs it actually emits.
- Once (1) is in place, helper attrs work for users who opt into the manual-derive form. Optionally, document the pattern in the README and the
#[gts_type_schema] rustdoc, with an example showing #[schemars(length(...))] / #[schemars(email)].
A more invasive alternative for (2) — out of scope for a first cut: re-emit user-supplied #[schemars(...)] field attrs into the macro output so the lint never fires, regardless of derive placement. But fixing (1) is enough to unblock real users.
Version
Observed on gts-macros = \"0.9.3\". Related (same crate): #85 (x-gts-traits-schema support).
Problem
#[gts_type_schema]/struct_to_gts_schemainjects#[derive(schemars::JsonSchema)]from inside the attribute-macro expansion. That has two downstream effects that together make it impossible to attach standard schemars validation helpers —#[schemars(length(min = 1, max = 255))],#[schemars(email)],#[schemars(extend(\"format\" = \"uuid\"))]— to struct fields.1. Helper-attribute ordering:
legacy_derive_helpersdeny#[schemars(...)]is a derive helper attribute: the parser only recognises it as legal once a corresponding#[derive(JsonSchema)]has been registered on the same item. Becausegts_type_schemainjects the derive at expansion time, at first parse the helpers look orphaned andrustcfires thelegacy_derive_helperslint (deny-by-default in 2024 edition). The user cannot use any schemars helper on a GTS struct's fields.2. Workaround "add the derive manually" doesn't work — duplicate impl
The obvious workaround is to put
#[derive(schemars::JsonSchema)]on the struct yourself, above#[gts_type_schema(...)].gts-macrosalready contains ahas_derivecheck (src/lib.rs:351) andadd_missing_derivesfilters by it (src/lib.rs:481-513), so in principle this should suppress the auto-injection. In practice on 0.9.3 we still get duplicateimpl JsonSchema for X→error[E0119]: conflicting implementations of trait.Reproducer (boiled down from
account-management-sdk::gts::UserV1):```rust
use modkit_gts::gts_type_schema; // wraps gts_macros::struct_to_gts_schema
#[derive(schemars::JsonSchema)] // try to claim the derive up-front
#[gts_type_schema(
dir_path = "schemas",
schema_id = "gts.cf.core.am.user.v1~",
description = "...",
properties = "id,username",
base = true,
)]
pub struct UserV1 {
pub id: gts::GtsInstanceId,
#[schemars(length(min = 1, max = 255))]
pub username: String,
}
```
Observed:
E0119onJsonSchema for UserV1. Expected (per the in-code comment onhas_derive): macro detects the user derive and skips its own.Why it matters
Real-world contract drift: hand-authored JSON Schemas in
docs/schemas/declareminLength,maxLength,format: emailon string fields. The Rust-generated mirror via#[gts_type_schema]produces a schema without those constraints, so runtime validators accept any non-empty string for length-constrained fields and any string foremail-formatted fields. Same shape will repeat for every future GTS base envelope that wants to express even basic field constraints. This is tracked in our codebase asTODO(am-user-schema-constraints).Proposal
Two parts, in priority order:
has_derivesuppression so a user-supplied#[derive(schemars::JsonSchema)](and likewiseSerialize/Deserializefor base structs) reliably prevents the macro's own injection. May require either (a) examining attrs after derive expansion, or (b) reordering so the macro'sadd_missing_derivesruns against the attrs it actually emits.#[gts_type_schema]rustdoc, with an example showing#[schemars(length(...))]/#[schemars(email)].A more invasive alternative for (2) — out of scope for a first cut: re-emit user-supplied
#[schemars(...)]field attrs into the macro output so the lint never fires, regardless of derive placement. But fixing (1) is enough to unblock real users.Version
Observed on
gts-macros = \"0.9.3\". Related (same crate): #85 (x-gts-traits-schemasupport).