Skip to content

struct_to_gts_schema: honor user-supplied #[derive(JsonSchema)] so schemars field helpers (length/email/extend) become usable #86

@diffora

Description

@diffora

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 Xerror[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:

  1. 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.
  2. 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).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions