Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions