You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
**Issue**: [#72 - gts_type field blocks Deserialize](https://github.com/GlobalTypeSystem/gts-rust/issues/72)
5
5
**Branch**: `gts-macro-proposal`
6
6
7
7
---
8
8
9
9
## 1. Purpose
10
10
11
-
This proposal replaces the `#[struct_to_gts_schema]`attribute macro with a `#[derive(GtsSchema)]` derive macro. The primary motivation is not a cosmetic redesign -- it is to correct assumptions in the current macro that contradict the GTS specification and to build a foundation that can grow with the spec.
11
+
The `#[struct_to_gts_schema]` macro has been the primary integration point between Rust structs and the Global Type System. It delivers compile-time validation, JSON Schema generation, and a runtime API from a single annotation. As the GTS specification has matured and usage has grown, several areas have emerged where the macro's assumptions can be brought into closer alignment with the spec.
12
12
13
-
The current macro enforces constraints the GTS specification explicitly leaves to implementations, silently manipulates user code in ways that cause bugs, and couples orthogonal concerns into a single monolithic invocation. This proposal decomposes the macro into focused, composable units that align with the spec and follow Rust ecosystem conventions.
13
+
This proposal evolves the macro from `#[struct_to_gts_schema]` to `#[derive(GtsSchema)]` with `#[gts(...)]` attributes. The goals are:
14
+
15
+
- Align field and identity requirements with the GTS specification (v0.8)
16
+
- Support the full range of GTS document categories (Spec SS11.1, Rule C)
- Give users explicit control over serde derives while preserving safety defaults
19
+
- Structure the codebase for future spec features like schema traits (SS9.7)
20
+
21
+
All existing compile-time validations, runtime behavior, and schema output are preserved. The old macro continues to work alongside the new one during migration.
14
22
15
23
---
16
24
17
25
## 2. Opportunities for Alignment
18
26
19
-
### 2.1 Mandatory identity fields contradict the specification
20
-
21
-
The current macro requires every base struct to declare either a `GtsSchemaId` field (for anonymous instances) or a `GtsInstanceId` field (for well-known instances). This is enforced at compile time:
27
+
### 2.1 Supporting all GTS document categories
22
28
23
-
```
24
-
Base structs must have either an ID field (one of: $id, id, gts_id, gtsId)
25
-
of type GtsInstanceId OR a GTS Type field (one of: type, gts_type, gtsType,
26
-
schema) of type GtsSchemaId
27
-
```
29
+
The current macro requires every base struct to declare either a `GtsSchemaId` field (for anonymous instances) or a `GtsInstanceId` field (for well-known instances). This was a reasonable default when the macro was written, as the primary use case was event types that always carry identity fields.
28
30
29
-
The GTS specification (v0.8) defines **five** categories of JSON documents (Spec SS11.1, Rule C). Only two of the five require identity fields:
31
+
However, the GTS specification (v0.8) defines **five** categories of JSON documents (Spec SS11.1, Rule C). Only two of the five require identity fields:
30
32
31
33
| Category | Identity field required? | Example |
32
34
|---|---|---|
@@ -36,20 +38,20 @@ The GTS specification (v0.8) defines **five** categories of JSON documents (Spec
36
38
| 4. **Well-known GTS instances**|**Yes** -- GTS instance ID in `id` field | Event topics, modules |
37
39
| 5. **Anonymous GTS instances**|**Yes** -- GTS type ID in `type` field | Events, audit records |
38
40
39
-
The spec includes concrete examples of GTS schemas whose instances have **no** GTS identity field:
41
+
The spec includes concrete examples of GTS schemas whose instances have no GTS identity field:
40
42
41
43
-`gts.x.commerce.orders.order.v1.0~` -- Order schema. The `id` field is a plain UUID, not a `GtsInstanceId`. There is no `type` field.
42
44
-`gts.x.core.idp.contact.v1.0~` -- Contact schema. Same pattern: UUID `id`, no GTS identity.
43
45
44
46
These are valid GTS entity schemas (category 1) that produce instances falling under category 3. They are referenced by other GTS types (e.g., an event's `subjectType` references the order schema) but their instances do not self-identify via GTS.
45
47
46
-
The spec is explicit about this being a design choice, not an oversight (SS11.1):
48
+
The spec notes this explicitly (SS11.1):
47
49
48
50
> *"The exact field names used for instance IDs and instance types are **implementation-defined** and may be **configuration-driven** (different systems may look for identifiers in different fields)."*
49
51
50
-
The macro's requirement is not grounded in the spec. It forces users into workarounds like Issue #72, where a dummy `gts_type` field must be added with fragile serde attributes just to satisfy the macro.
52
+
This gap surfaced as Issue #72, where data entity structs are forced to add a dummy `gts_type` field with fragile serde workarounds to satisfy the macro's requirement.
51
53
52
-
### 2.2 No distinction between self-reference and cross-reference
54
+
### 2.2 Distinguishing self-reference from cross-reference
53
55
54
56
The GTS specification defines two kinds of `x-gts-ref` annotations on schema properties (SS9.6):
55
57
@@ -84,39 +86,42 @@ The module schema (`gts.x.core.modules.module.v1~.schema.json`) shows the same p
84
86
}
85
87
```
86
88
87
-
The current macro treats **all**`GtsSchemaId` fields identically, generating `"x-gts-ref": "gts.*"` for every one. It has no mechanism to distinguish a field that identifies *this* entity from a field that references *another* entity.
89
+
The current macro treats all `GtsSchemaId` fields identically, generating `"x-gts-ref": "gts.*"` for every one. It does not yet have a mechanism to distinguish a field that identifies *this* entity from a field that references *another* entity. This proposal adds that mechanism through field-level annotations.
88
90
89
-
### 2.3 Hidden serde manipulation
91
+
### 2.3 Making serde derives visible
90
92
91
-
The current macro silently adds `Serialize`, `Deserialize`, and `JsonSchema` derives to base structs, and silently removes `Serialize`/`Deserialize`from nested structs. This means:
93
+
The current macro automatically adds `Serialize`, `Deserialize`, and `JsonSchema` derives to base structs, and blocks `Serialize`/`Deserialize`on nested structs. This approach successfully prevents nested structs from producing incomplete JSON, which was the original design goal.
92
94
93
-
- Users cannot see which traits are derived by reading the struct definition
94
-
- Adding `Serialize` to a nested struct for testing is silently stripped
95
-
- The macro's serde attribute injection (`#[serde(bound(...))]`, `#[serde(serialize_with)]`) is invisible in source code
96
-
- Issue #72 exists precisely because the macro's serde injection for identity fields doesn't handle deserialization correctly
95
+
The tradeoff is that users cannot see which traits are derived by reading the struct definition. The macro's serde attribute injection (`#[serde(bound(...))]`, `#[serde(serialize_with)]`) is invisible in source code. Issue #72 arose in part because the macro's serde handling for identity fields didn't account for deserialization correctly -- a problem that's harder to diagnose when the serde configuration isn't visible.
97
96
98
-
### 2.4 Redundant properties parameter
97
+
This proposal makes all derives explicit while preserving the same safety default: nested structs are still blocked from direct serialization unless the user opts out with `allow_direct_serde`.
99
98
100
-
The macro requires `properties = "event_type,id,tenant_id,payload"` -- a comma-separated string that duplicates the struct's field list. If a field is added to the struct but omitted from `properties`, it silently disappears from the generated JSON Schema. The macro catches the inverse (a property listed that doesn't exist as a field), but the more dangerous case -- a forgotten field -- is not caught.
The current macro requires `properties = "event_type,id,tenant_id,payload"` -- a comma-separated string that lists which fields appear in the schema. This serves as both a schema surface declaration and a typo check (the macro validates that every listed property exists as a field).
103
102
104
-
The `base` attribute conflates two orthogonal concepts:
103
+
The tradeoff is that the more dangerous direction isn't caught: if a field is added to the struct but omitted from `properties`, it silently disappears from the generated JSON Schema. For a system focused on diffable API contracts, this means a schema diff would show no change even though the wire format changed.
105
104
106
-
|`base` value | GTS meaning | Serialization meaning |
107
-
|---|---|---|
108
-
|`base = true`| Root type in hierarchy | Gets `Serialize`/`Deserialize`|
109
-
|`base = ParentStruct`| Child type inheriting from parent | Blocked from direct serialization |
105
+
This proposal auto-derives properties from struct fields, catching changes in both directions. Fields can be excluded from the schema with `#[gts(skip)]` or `#[serde(skip)]`.
106
+
107
+
### 2.5 Clearer inheritance declaration
108
+
109
+
The current macro uses `base` to declare a struct's position in the hierarchy:
110
+
111
+
|`base` value | Meaning |
112
+
|---|---|
113
+
|`base = true`| Root type (no parent) |
114
+
|`base = ParentStruct`| Child type inheriting from parent |
110
115
111
-
`base = true` carries no information-- it is the default state. `base = ParentStruct`uses the word "base" to mean the opposite of what it says.
116
+
`base = true`is the default state and carries no information. This proposal removes the need to declare root types explicitly -- the absence of `extends` means root -- and uses `extends = ParentStruct`for child types, which reads more naturally in the context of GTS's left-to-right inheritance model (SS2.2, SS3.2).
112
117
113
118
---
114
119
115
-
## 3. What the Proposal Changes
120
+
## 3. What Changes
116
121
117
122
### 3.1 Entry point: Derive macro with `#[gts(...)]` attributes
118
123
119
-
The single `#[struct_to_gts_schema]` attribute macro is replaced with `#[derive(GtsSchema)]`and`#[gts(...)]` attributes at both the struct and field level.
124
+
The single `#[struct_to_gts_schema]` attribute macro evolves into `#[derive(GtsSchema)]`with`#[gts(...)]` attributes at both the struct and field level.
120
125
121
126
**Before:**
122
127
@@ -247,7 +252,7 @@ pub struct QuotaViolationV1 {
247
252
}
248
253
```
249
254
250
-
No dummy field. No serde workaround. The struct represents exactly what the GTS spec intends -- a data entity schema whose instances don't carry GTS identity fields, like `order.v1.0~` or `contact.v1.0~` in the spec examples.
255
+
No dummy field. No serde workaround. The struct represents what the GTS spec intends -- a data entity schema whose instances don't carry GTS identity fields, like `order.v1.0~` or `contact.v1.0~` in the spec examples.
251
256
252
257
When identity fields *are* needed, they are annotated explicitly:
253
258
@@ -281,7 +286,7 @@ The field-level attributes are validated at compile time:
281
286
282
287
The macro no longer injects or removes serde derives. Users explicitly declare `Serialize` and `Deserialize` where needed.
283
288
284
-
Nested structs (those with `extends`) are still blocked from direct serialization by default -- serializing a nested payload alone produces incomplete JSON (missing the base event envelope). This is enforced via marker trait conflicts (`GtsNoDirectSerialize` / `GtsNoDirectDeserialize`). The user can opt out with `allow_direct_serde` for testing or standalone use:
289
+
Nested structs (those with `extends`) are still blocked from direct serialization by default -- serializing a nested payload alone produces incomplete JSON (missing the base event envelope). This safety behavior, which was an intentional and valuable part of the original design, is preserved via marker trait conflicts (`GtsNoDirectSerialize` / `GtsNoDirectDeserialize`). The user can opt out with `allow_direct_serde` for testing or standalone use:
@@ -297,7 +302,7 @@ Without `allow_direct_serde`, deriving `Serialize` on a nested struct produces a
297
302
298
303
### 3.5 Auto-derived properties
299
304
300
-
The `properties` parameter is removed. All named struct fields are included in the generated JSON Schema by default. To exclude a field:
305
+
The `properties` parameter is replaced with auto-derivation from struct fields. All named fields are included in the generated JSON Schema by default. To exclude a field:
301
306
302
307
```rust
303
308
#[gts(skip)] // excluded from schema, still serializable
@@ -325,7 +330,7 @@ The generated JSON Schemas are **structurally identical** between old and new ma
325
330
326
331
### 4.2 Improvements
327
332
328
-
**`description` included in runtime schemas.** The old macro stores the `description` attribute but omits it from `gts_schema_with_refs()` output. The new macro includes it, consistent with every spec example schema (`events.type.v1~`, `events.topic.v1~`, `orders.order.v1.0~`, `modules.module.v1~` -- all include `description`).
333
+
**`description` included in runtime schemas.** The current macro stores the `description` attribute but omits it from `gts_schema_with_refs()` output. The updated macro includes it, consistent with every spec example schema (`events.type.v1~`, `events.topic.v1~`, `orders.order.v1.0~`, `modules.module.v1~` -- all include `description`).
329
334
330
335
**Spec-correct `x-gts-ref` on identity fields.** As described in section 3.3, annotated identity fields generate `"x-gts-ref": "/$id"` while unannotated `GtsSchemaId` fields retain `"x-gts-ref": "gts.*"`.
331
336
@@ -364,17 +369,17 @@ Compare the `type` property above with the spec's base event schema (`gts.x.core
364
369
}
365
370
```
366
371
367
-
Both use `"x-gts-ref": "/$id"` on the type discriminator field. The old macro would generate`"x-gts-ref": "gts.*"` here.
372
+
Both use `"x-gts-ref": "/$id"` on the type discriminator field. The current macro generates`"x-gts-ref": "gts.*"` here.
368
373
369
374
---
370
375
371
376
## 5. Extensibility
372
377
373
-
The old macro is ~1,843 lines in a single file (`lib.rs`). The new implementation is split into focused modules:
378
+
The current macro is ~1,843 lines in a single file (`lib.rs`). The updated implementation splits into focused modules:
The **17 parity tests** are the most critical -- they define equivalent structs using both macros and assert identical schema output, serialization output, deserialization behavior, trait constants, and runtime API results.
@@ -450,7 +455,7 @@ The **17 parity tests** are the most critical -- they define equivalent structs
450
455
451
456
## 8. Migration
452
457
453
-
Both macros coexist. The old macro continues to work without changes.
458
+
Both macros coexist. The current macro continues to work without changes.
454
459
455
460
Migration per struct:
456
461
@@ -459,7 +464,7 @@ Migration per struct:
459
464
3. Replace `base = true` with nothing; replace `base = Parent` with `extends = Parent`
460
465
4. Remove `properties = "..."` -- add `#[gts(skip)]` to fields that were excluded
461
466
5. Add `#[gts(type_field)]` or `#[gts(instance_id)]` to identity fields if present
462
-
6. Remove dummy identity fields that existed only to satisfy the old macro's requirement
467
+
6. Remove dummy identity fields that existed only to satisfy the current macro's requirement
0 commit comments