| Crates.io | gts-macros |
| lib.rs | gts-macros |
| version | 0.1.0 |
| created_at | 2026-01-23 19:58:37.321507+00 |
| updated_at | 2026-01-23 19:58:37.321507+00 |
| description | Procedural macros for GTS schema generation |
| homepage | |
| repository | https://github.com/GlobalTypeSystem/gts-rust |
| max_upload_size | |
| id | 2065372 |
| size | 321,659 |
Procedural macros for GTS (Global Type System) schema generation from Rust structs.
The #[struct_to_gts_schema] attribute macro serves three purposes:
Add to your Cargo.toml:
[dependencies]
gts-macros = { path = "path/to/gts-rust/gts-macros" }
serde = { version = "1.0", features = ["derive"] }
use gts_macros::struct_to_gts_schema;
use uuid::Uuid;
use gts::gts::{GtsInstanceId, GtsSchemaId};
// Base event type (root of the hierarchy)
// Note: #[derive(Serialize, Deserialize, JsonSchema)] is added automatically!
#[derive(Debug)]
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event type with common fields",
properties = "id,tenant_id,payload"
)]
pub struct BaseEventV1<P> {
pub id: Uuid,
pub r#type: GtsSchemaId,
pub tenant_id: Uuid,
pub payload: P,
}
// Audit event that inherits from BaseEventV1
#[derive(Debug)]
#[struct_to_gts_schema(
dir_path = "schemas",
base = BaseEventV1,
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
description = "Audit event with user context",
properties = "user_id,action"
)]
pub struct AuditEventV1 {
pub user_id: Uuid,
pub action: String,
}
// Runtime usage:
fn example() {
// Access schema IDs
let base_id = BaseEventV1::<()>::gts_schema_id();
let audit_id = AuditEventV1::gts_schema_id();
// Get schemas as JSON
let base_schema = BaseEventV1::<()>::gts_schema_with_refs_as_string_pretty();
let audit_schema = AuditEventV1::gts_schema_with_refs_as_string_pretty();
// Generate instance IDs
let event_id = AuditEventV1::gts_make_instance_id("evt-12345.v1");
assert_eq!(event_id.as_ref(), "gts.x.core.events.type.v1~x.core.audit.event.v1~evt-12345.v1");
}
The macro validates your annotations at compile time, catching errors early.
The macro automatically adds these derives to your struct:
serde::Serializeserde::Deserializeschemars::JsonSchemaDo NOT add these derives manually - they will conflict. You can add other derives like Debug, Clone, etc.
// ✅ Correct - only add Debug, Clone, etc.
#[derive(Debug, Clone)]
#[struct_to_gts_schema(...)]
pub struct MyStructV1 { ... }
// ❌ Wrong - will cause duplicate implementation errors
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[struct_to_gts_schema(...)]
pub struct MyStructV1 { ... }
| Check | Description |
|---|---|
| Required parameters | All of dir_path, base, schema_id, description, properties must be present |
| Base consistency | base = true requires single-segment schema_id; base = Parent requires multi-segment |
| Parent schema match | When base = Parent, Parent's SCHEMA_ID must match the parent segment in schema_id |
| Property existence | Every property in the list must exist as a field in the struct |
| Struct type | Only structs with named fields are supported (no tuple structs) |
| Generic type constraints | Generic type parameters must implement GtsSchema (only () or other GTS structs allowed) |
| Base struct field validation | Base structs (base = true) must have either ID fields OR GTS Type fields, but not both (see below) |
Missing property:
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event",
properties = "id,nonexistent" // ❌ Error!
)]
pub struct BaseEventV1<P> {
pub id: Uuid,
pub r#type: GtsSchemaId,
pub payload: P,
}
error: struct_to_gts_schema: Property 'nonexistent' not found in struct.
Available fields: ["id", "payload"]
Base mismatch (base = true with multi-segment schema_id):
#[struct_to_gts_schema(
dir_path = "schemas",
base = true, // ❌ Error! base = true requires single-segment
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
description = "Audit event",
properties = "user_id"
)]
pub struct AuditEventV1 { /* ... */ }
error: struct_to_gts_schema: base = true requires single-segment schema_id,
but found 2 segments
Parent schema ID mismatch:
#[struct_to_gts_schema(
dir_path = "schemas",
base = WrongParent, // ❌ Error! Parent's SCHEMA_ID doesn't match
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
description = "Audit event",
properties = "user_id"
)]
pub struct AuditEventV1 { /* ... */ }
error: struct_to_gts_schema: Base struct 'WrongParent' schema ID must match
parent segment 'gts.x.core.events.type.v1~' from schema_id
Tuple struct:
#[struct_to_gts_schema(/* ... */)]
pub struct Data(String); // ❌ Tuple struct not supported
error: struct_to_gts_schema: Only structs with named fields are supported
Non-GTS struct as generic argument:
// Regular struct without struct_to_gts_schema
pub struct MyStruct { pub some_id: String }
// Using it as generic argument fails
let event: BaseEventV1<MyStruct> = BaseEventV1 { /* ... */ }; // ❌ Error!
error[E0277]: the trait bound `MyStruct: GtsSchema` is not satisfied
--> src/main.rs:10:17
|
10 | let event: BaseEventV1<MyStruct> = BaseEventV1 { ... };
| ^^^^^^^^^^^^^^^^^^^^^ the trait `GtsSchema` is not implemented for `MyStruct`
Base struct field validation - ID fields:
use gts::gts::GtsInstanceId;
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.topic.v1~",
description = "Base event with ID field",
properties = "id,name"
)]
pub struct BaseEventTopicV1<P> {
pub id: GtsInstanceId, // ✅ Valid ID field
pub name: String,
pub payload: P,
}
Base struct field validation - GTS Type fields:
use gts::gts::GtsSchemaId;
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event with type field",
properties = "r#type,name"
)]
pub struct BaseEventV1<P> {
pub id: Uuid, // Event UUID
pub r#type: GtsSchemaId, // Event Type - ✅ Valid GTS Type field
pub name: String,
pub payload: P,
}
Invalid base struct - both ID and GTS Type fields:
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.topic.v1~",
description = "Invalid base with both ID and type",
properties = "id,r#type,name" // ❌ Error! Both ID and GTS Type fields
)]
pub struct BaseEventV1<P> {
pub id: GtsInstanceId, // Event topic ID field
pub r#type: GtsSchemaId, // Event type (schema) ID field - ❌ Cannot have both!
Invalid base struct - wrong GTS Type field type:
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event with wrong type field",
properties = "r#type,name"
)]
pub struct BaseEventV1<P> {
pub id: Uuid, // Event UUID
pub r#type: String, // Event type (schema) - ❌ Should be GtsSchemaId
pub name: String,
pub payload: P,
}
error: struct_to_gts_schema: Base structs with GTS Type fields must have at least one GTS Type field (type, gts_type, gtsType, or schema) of type GtsSchemaId
Base structs (base = true) must follow exactly one of these patterns:
$id, id, gts_id, gtsIdGtsInstanceId (or gts::GtsInstanceId)type, r#type, gts_type, gtsType, schema#[serde(rename = "type")], #[serde(rename = "gts_type")], #[serde(rename = "gtsType")], or #[serde(rename = "schema")]GtsSchemaId (or gts::GtsSchemaId)Serde rename example:
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event with serde(rename = \"type\")",
properties = "event_type,id,tenant_id,sequence_id,payload"
)]
pub struct BaseEventV1<P> {
#[serde(rename = "type")]
pub event_type: GtsSchemaId, // ✅ Valid - renamed to "type"
pub id: Uuid,
pub tenant_id: Uuid,
pub sequence_id: u64,
pub payload: P,
}
Important: Base structs cannot have both ID fields AND GTS Type fields. They must choose one approach.
Generate JSON Schema files using the GTS CLI tool.
# Using paths from macro (relative to source files)
gts generate-from-rust --source src/
# Override output directory
gts generate-from-rust --source src/ --output schemas/
# Exclude specific directories (can be used multiple times)
gts generate-from-rust --source . --exclude "tests/*" --exclude "examples/*"
# Using cargo
cargo run --bin gts -- generate-from-rust --source src/
The CLI provides multiple ways to exclude files from scanning:
1. --exclude option (supports glob patterns):
gts generate-from-rust --source . --exclude "tests/*" --exclude "benches/*"
2. Auto-ignored directories: The following directories are automatically skipped:
compile_fail/ - trybuild compile-fail tests3. // gts:ignore directive: Add this comment at the top of any .rs file:
// gts:ignore
//! This file will be skipped by the CLI
use gts_macros::struct_to_gts_schema;
// ...
#[struct_to_gts_schema] annotationsdir_path/<schema_id>.schema.jsonBase event type (schemas/gts.x.core.events.type.v1~.schema.json):
{
"$id": "gts://gts.x.core.events.type.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "BaseEventV1",
"type": "object",
"description": "Base event type with common fields",
"properties": {
"id": { "type": "string", "format": "uuid" },
"tenant_id": { "type": "string", "format": "uuid" },
"payload": { "type": "object" }
},
"required": ["id", "tenant_id", "payload"]
}
Inherited audit event (schemas/gts.x.core.events.type.v1~x.core.audit.event.v1~.schema.json):
{
"$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuditEventV1",
"type": "object",
"description": "Audit event with user context",
"allOf": [
{ "$ref": "gts://gts.x.core.events.type.v1~" },
{
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"properties": {
"user_id": { "type": "string", "format": "uuid" },
"action": { "type": "string" }
},
"required": ["user_id", "action"]
}
}
}
]
}
The CLI automatically maps Rust types to JSON Schema types:
| Rust Type | JSON Schema Type | Format | Required |
|---|---|---|---|
String, &str |
string |
- | Yes |
i8-i128, u8-u128 |
integer |
- | Yes |
f32, f64 |
number |
- | Yes |
bool |
boolean |
- | Yes |
Vec<T> |
array |
- | Yes |
Option<T> |
Same as T |
- | No |
Uuid |
string |
uuid |
Yes |
DateTime, NaiveDateTime |
string |
date-time |
Yes |
NaiveDate |
string |
date |
Yes |
HashMap<K,V>, BTreeMap<K,V> |
object |
- | Yes |
GtsInstanceId |
string |
gts-instance-id |
Yes |
GtsSchemaId |
string |
gts-schema-id |
Yes |
Notes:
Option<T> fields are not marked as required in the generated schemaP in BaseEventV1<P>) are mapped to {"type": "object"} placeholdersThe macro generates associated constants, methods, and implements the GtsSchema trait for runtime use.
Get the struct's GTS schema ID:
// Using gts_schema_id() - returns &'static GtsSchemaId
let schema_id: >s::gts::GtsSchemaId = AuditEventV1::gts_schema_id();
println!("Schema ID: {}", schema_id.as_ref());
// Output: gts.x.core.events.type.v1~x.core.audit.event.v1~
// For generic structs, use () as type parameter
let base_id = BaseEventV1::<()>::gts_schema_id();
println!("Base schema ID: {}", base_id.as_ref());
// Output: gts.x.core.events.type.v1~
Get the parent (base) schema ID:
// Using gts_base_schema_id() - returns Option<&'static GtsSchemaId>
let parent_id: Option<>s::gts::GtsSchemaId> = AuditEventV1::gts_base_schema_id();
match parent_id {
Some(id) => println!("Parent schema ID: {}", id.as_ref()),
// Output: gts.x.core.events.type.v1~
None => println!("This is a base struct (no parent)"),
}
// Base structs return None
assert!(BaseEventV1::<()>::gts_base_schema_id().is_none());
// Child structs return Some(&GtsSchemaId)
assert_eq!(
AuditEventV1::gts_base_schema_id().map(|id| id.as_ref()),
Some("gts.x.core.events.type.v1~")
);
Get the struct's JSON schema with references:
use gts::GtsSchema;
// Using gts_schema_with_refs() - returns serde_json::Value
let schema_value = AuditEventV1::gts_schema_with_refs();
println!("Schema $id: {}", schema_value["$id"]);
// Output: gts://gts.x.core.events.type.v1~x.core.audit.event.v1~
// Using gts_schema_with_refs_as_string() - returns compact JSON String
let schema_json = AuditEventV1::gts_schema_with_refs_as_string();
println!("Schema JSON: {}", schema_json);
// Using gts_schema_with_refs_as_string_pretty() - returns pretty-printed JSON String
let schema_pretty = AuditEventV1::gts_schema_with_refs_as_string_pretty();
println!("Pretty schema:\n{}", schema_pretty);
// For generic structs, the type parameter doesn't affect the schema
// Both generate identical schemas:
let schema1 = BaseEventV1::<()>::gts_schema_with_refs();
let schema2 = BaseEventV1::<AuditEventV1>::gts_schema_with_refs();
assert_eq!(schema1, schema2); // OK. Identical schemas
Schema structure:
Base structs (single-segment schema_id): Direct properties, no allOf
{
"$id": "gts://gts.x.core.events.type.v1~",
"type": "object",
"additionalProperties": false,
"properties": { /* ... */ }
}
Child structs (multi-segment schema_id): Uses allOf with $ref to parent
{
"$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
"type": "object",
"allOf": [
{ "$ref": "gts://gts.x.core.events.type.v1~" },
{ "properties": { /* child-specific properties */ } }
]
}
Important: Nested payload structs (like AuditPayloadV1, PlaceOrderDataV1) should never be serialized/deserialized alone. Always serialize/deserialize the complete event hierarchy starting from the base event.
Serialize complete event instances to JSON:
use serde::{Serialize, Deserialize};
use uuid::Uuid;
// Create a complete event with nested payloads
// BaseEventV1 -> AuditPayloadV1 -> PlaceOrderDataV1
let event = BaseEventV1 {
event_type: PlaceOrderDataV1::gts_schema_id().clone(),
id: Uuid::new_v4(),
tenant_id: Uuid::new_v4(),
sequence_id: 42,
payload: AuditPayloadV1 {
user_agent: "Mozilla/5.0".to_string(),
user_id: Uuid::new_v4(),
ip_address: "192.168.1.1".to_string(),
data: PlaceOrderDataV1 {
order_id: Uuid::new_v4(),
product_id: Uuid::new_v4(),
},
},
};
// Using GTS instance methods (recommended):
// gts_instance_json() - returns serde_json::Value
let json_value = event.gts_instance_json();
println!("Value: {:?}", json_value);
// gts_instance_json_as_string() - returns compact JSON String
let json_string = event.gts_instance_json_as_string();
println!("JSON: {}", json_string);
// gts_instance_json_as_string_pretty() - returns pretty-printed JSON String
let json_pretty = event.gts_instance_json_as_string_pretty();
println!("Pretty JSON:\n{}", json_pretty);
// You can also use serde_json directly:
let json_string = serde_json::to_string(&event).unwrap();
let json_pretty = serde_json::to_string_pretty(&event).unwrap();
let json_value = serde_json::to_value(&event).unwrap();
Deserialize JSON to complete event instances:
// Deserialize from JSON string
let json_str = r#"{
"type": "gts.x.core.events.type.v1~x.core.audit.event.v1~x.marketplace.orders.purchase.v1~",
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "660e8400-e29b-41d4-a716-446655440000",
"sequence_id": 42,
"payload": {
"user_agent": "Mozilla/5.0",
"user_id": "770e8400-e29b-41d4-a716-446655440000",
"ip_address": "192.168.1.1",
"data": {
"order_id": "880e8400-e29b-41d4-a716-446655440000",
"product_id": "990e8400-e29b-41d4-a716-446655440000"
}
}
}"#;
let event: BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>> =
serde_json::from_str(json_str).unwrap();
println!("Deserialized: {:?}", event);
// Deserialize from serde_json::Value
let json_value = serde_json::json!({
"type": "gts.x.core.events.type.v1~x.core.audit.event.v1~x.marketplace.orders.purchase.v1~",
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "660e8400-e29b-41d4-a716-446655440000",
"sequence_id": 42,
"payload": {
"user_agent": "Mozilla/5.0",
"user_id": "770e8400-e29b-41d4-a716-446655440000",
"ip_address": "192.168.1.1",
"data": {
"order_id": "880e8400-e29b-41d4-a716-446655440000",
"product_id": "990e8400-e29b-41d4-a716-446655440000"
}
}
});
let event: BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>> =
serde_json::from_value(json_value).unwrap();
println!("Deserialized: {:?}", event);
// Deserialize from reader (file, network stream, etc.)
use std::fs::File;
let file = File::open("event.json").unwrap();
let event: BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>> =
serde_json::from_reader(file).unwrap();
Working with different payload types:
// You can use different payload combinations with the same base event
type OrderEvent = BaseEventV1<AuditPayloadV1<PlaceOrderDataV1>>;
type SimpleEvent = BaseEventV1<()>;
// Create and serialize different event types
let order_event: OrderEvent = BaseEventV1 {
event_type: PlaceOrderDataV1::gts_schema_id().clone(),
id: Uuid::new_v4(),
tenant_id: Uuid::new_v4(),
sequence_id: 1,
payload: AuditPayloadV1 {
user_agent: "Mozilla/5.0".to_string(),
user_id: Uuid::new_v4(),
ip_address: "192.168.1.1".to_string(),
data: PlaceOrderDataV1 {
order_id: Uuid::new_v4(),
product_id: Uuid::new_v4(),
},
},
};
let json = serde_json::to_string_pretty(&order_event).unwrap();
println!("Order event JSON:\n{}", json);
// Deserialize back to the correct type
let deserialized: OrderEvent = serde_json::from_str(&json).unwrap();
Generate instance IDs by appending a segment to the schema ID:
// Generate event instance ID
let topic_id = AuditEventTopicV1::gts_make_instance_id("x.core.audit.topic.v1");
assert_eq!(
topic_id.as_ref(),
"gts.x.core.events.topic.v1~x.core.audit.topic.v1"
);
// Generate base event instance ID
let base_id = BaseEventTopicV1::<()>::gts_make_instance_id("a.b.c.topic.v1");
assert_eq!(base_id.as_ref(), "gts.x.core.events.topic.v1~a.b.c.topic.v1");
// Convert to String when needed
let id_string: String = topic_id.clone().into();
// Use as map key (implements Hash, Eq, Clone)
use std::collections::HashMap;
let mut events: HashMap<gts::GtsInstanceId, String> = HashMap::new();
events.insert(event_id, "processed".to_owned());
// Serialize/deserialize instance IDs
let id_json = serde_json::to_string(&event_id).unwrap();
let id_back: gts::GtsInstanceId = serde_json::from_str(&id_json).unwrap();
GtsSchema Trait)The macro automatically implements the GtsSchema trait, enabling runtime schema composition for nested generic types:
use gts::GtsSchema;
// Get composed schema for nested type
let schema = BaseEventV1::<AuditPayloadV1<PlaceOrderDataV1>>::gts_schema_with_refs_allof();
// The schema will have proper nesting:
// - payload field contains AuditPayloadV1's schema
// - payload.data field contains PlaceOrderDataV1's schema
// - All with additionalProperties: false for type safety
Generic Field Type Safety: Generic fields (fields that accept nested types) automatically have additionalProperties: false set. This ensures:
| API | Type | Description |
|---|---|---|
gts_schema_id() |
&'static GtsSchemaId |
Get the struct's GTS schema ID |
gts_base_schema_id() |
Option<&'static GtsSchemaId> |
Get parent schema ID (None for base structs) |
gts_schema_with_refs() |
serde_json::Value |
Get schema as JSON value with $ref |
gts_schema_with_refs_as_string() |
String |
Get schema as compact JSON string |
gts_schema_with_refs_as_string_pretty() |
String |
Get schema as pretty-printed JSON string |
gts_instance_json(&self) |
serde_json::Value |
Serialize instance to JSON value |
gts_instance_json_as_string(&self) |
String |
Serialize instance to compact JSON string |
gts_instance_json_as_string_pretty(&self) |
String |
Serialize instance to pretty-printed JSON string |
gts_make_instance_id(segment) |
GtsInstanceId |
Generate instance ID by appending segment |
All parameters are required (5 total):
| Parameter | Description | Example |
|---|---|---|
dir_path |
Output directory for generated schema | "schemas" |
base |
Inheritance declaration (see below) | true or ParentStruct |
schema_id |
GTS identifier | "gts.x.app.entities.user.v1~" |
description |
Human-readable description | "User entity" |
properties |
Comma-separated field list | "id,email,name" |
base AttributeThe base attribute explicitly declares the struct's position in the inheritance hierarchy:
| Value | Meaning | Schema ID Requirement |
|---|---|---|
base = true |
This is a root/base type (no parent) | Single-segment (e.g., gts.x.core.events.type.v1~) |
base = ParentStruct |
This inherits from ParentStruct |
Multi-segment (e.g., gts.x.core.events.type.v1~x.core.audit.event.v1~) |
Compile-time validation: The macro validates that:
base = true requires a single-segment schema_idbase = ParentStruct requires a multi-segment schema_id where the parent segment matches ParentStruct's SCHEMA_IDgts.<vendor>.<package>.<namespace>.<type>.v<MAJOR>[.<MINOR>]~
Examples:
gts.x.core.iam.user.v1~ - IAM user schemagts.x.commerce.orders.order.v1.0~ - Order schema with minor version// src/events.rs
use gts_macros::struct_to_gts_schema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
// Base event type - the root of all events
#[derive(Debug, Serialize, Deserialize)]
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event type with common fields",
properties = "id,tenant_id,timestamp,payload"
)]
pub struct BaseEventV1<P> {
pub id: Uuid,
pub r#type: GtsSchemaId,
pub tenant_id: Uuid,
pub timestamp: String,
pub payload: P,
}
// Audit event - extends BaseEventV1 with user context
#[derive(Debug, Serialize, Deserialize)]
#[struct_to_gts_schema(
dir_path = "schemas",
base = BaseEventV1,
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
description = "Audit event with user tracking",
properties = "user_id,ip_address,action"
)]
pub struct AuditEventV1<D> {
pub user_id: Uuid,
pub ip_address: String,
pub action: D,
}
// Order placed event - extends AuditEventV1 for order actions
#[derive(Debug, Serialize, Deserialize)]
#[struct_to_gts_schema(
dir_path = "schemas",
base = AuditEventV1,
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~x.shop.orders.placed.v1~",
description = "Order placement event",
properties = "order_id,total"
)]
pub struct OrderPlacedV1 {
pub order_id: Uuid,
pub total: f64,
}
gts generate-from-rust --source src/
# Output:
# Generated schema: gts.x.core.events.type.v1~ @ schemas/...
# Generated schema: gts.x.core.events.type.v1~x.core.audit.event.v1~ @ schemas/...
# Generated schema: gts.x.core.events.type.v1~x.core.audit.event.v1~x.shop.orders.placed.v1~ @ schemas/...
fn main() {
// Access schemas at any level
println!("Base event schema: {}", BaseEventV1::<()>::gts_schema_with_refs_as_string_pretty());
println!("Audit event schema: {}", AuditEventV1::<()>::gts_schema_with_refs_as_string_pretty());
println!("Order placed schema: {}", OrderPlacedV1::gts_schema_with_refs_as_string_pretty());
// Generate instance IDs
let event_id = OrderPlacedV1::gts_make_instance_id("evt-12345.v1");
println!("Event ID: {}", event_id);
// Output: gts.x.core.events.type.v1~x.core.audit.event.v1~x.shop.orders.placed.v1~evt-12345.v1
// Use as HashMap key
use std::collections::HashMap;
let mut events: HashMap<gts::GtsInstanceId, String> = HashMap::new();
events.insert(event_id, "processed".to_owned());
}
The macro supports explicit inheritance declaration through the base attribute and provides compile-time validation to ensure parent-child relationships are correct.
See tests/inheritance_tests.rs for a complete working example:
// Base event type (base = true, single-segment schema_id)
#[struct_to_gts_schema(
dir_path = "schemas",
base = true,
schema_id = "gts.x.core.events.type.v1~",
description = "Base event type definition",
properties = "event_type,id,tenant_id,sequence_id,payload"
)]
pub struct BaseEventV1<P> {
#[serde(rename = "type")]
pub event_type: GtsSchemaId,
pub id: Uuid,
pub tenant_id: Uuid,
pub sequence_id: u64,
pub payload: P,
}
// Extends BaseEventV1 (base = ParentStruct, multi-segment schema_id)
#[struct_to_gts_schema(
dir_path = "schemas",
base = BaseEventV1,
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~",
description = "Audit event with user context",
properties = "user_agent,user_id,ip_address,data"
)]
pub struct AuditPayloadV1<D> {
pub user_agent: String,
pub user_id: Uuid,
pub ip_address: String,
pub data: D,
}
// Extends AuditPayloadV1 (3-level inheritance chain)
#[struct_to_gts_schema(
dir_path = "schemas",
base = AuditPayloadV1,
schema_id = "gts.x.core.events.type.v1~x.core.audit.event.v1~x.marketplace.orders.purchase.v1~",
description = "Order placement audit event",
properties = "order_id,product_id"
)]
pub struct PlaceOrderDataV1 {
pub order_id: Uuid,
pub product_id: Uuid,
}
Single-segment schema (no inheritance):
{
"$id": "gts://gts.x.core.events.type.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "BaseEventV1",
"type": "object",
"description": "Base event type definition",
"properties": { /* direct properties */ },
"required": [ /* required fields */ ]
}
Multi-segment schema (with inheritance):
{
"$id": "gts://gts.x.core.events.type.v1~x.core.audit.event.v1~",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuditPayloadV1",
"type": "object",
"description": "Audit event with user context",
"allOf": [
{ "$ref": "gts://gts.x.core.events.type.v1~" },
{
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"properties": { /* child-specific properties */ },
"required": [ /* child-specific required fields */ ]
}
}
}
]
}
Important: Generic fields (fields that accept nested types) automatically have additionalProperties: false set. This ensures that only properly nested inherited structs can be used, preventing arbitrary extra properties from being added to generic fields.
The macro validates your configuration at compile time, preventing runtime errors:
| ✅ Guaranteed | ❌ Prevented |
|---|---|
| All required attributes exist | Missing dir_path, base, schema_id, description, or properties |
| Base attribute consistency | base = true with multi-segment schema_id, or base = Parent with single-segment |
| Parent schema ID match | base = Parent where Parent's SCHEMA_ID doesn't match the parent segment |
| Properties exist in struct | Referencing non-existent fields in properties list |
| Valid struct types | Tuple structs, unit structs, enums |
| Single generic parameter | Multiple type generics (prevents inheritance ambiguity) |
| Valid GTS ID format | Malformed schema identifiers |
| Memory efficiency | No unnecessary allocations in generated constants |
| Strict generic field validation | Generic fields have additionalProperties: false to ensure only nested inherited structs are allowed |
| GTS-only generic arguments | Using non-GTS structs as generic type parameters (see below) |
The macro automatically adds a GtsSchema trait bound to all generic type parameters. This ensures that only valid GTS types can be used as generic arguments:
// ✅ Allowed: () is a valid GTS type (terminates the chain)
let event: BaseEventV1<()> = BaseEventV1 { /* ... */ };
// ✅ Allowed: AuditPayloadV1 has struct_to_gts_schema applied
let event: BaseEventV1<AuditPayloadV1<()>> = BaseEventV1 { /* ... */ };
// ❌ Compile error: MyStruct does not implement GtsSchema
pub struct MyStruct { pub some_id: String }
let event: BaseEventV1<MyStruct> = BaseEventV1 { /* ... */ };
// error: the trait bound `MyStruct: GtsSchema` is not satisfied
This prevents accidental use of arbitrary structs that haven't been properly annotated with struct_to_gts_schema, ensuring type safety across the entire GTS inheritance chain.
additionalPropertiesWhen a struct has a generic type parameter (e.g., BaseEventV1<P> with field payload: P), the generated schema sets additionalProperties: false on that field's schema. This ensures:
Example:
{
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"properties": { /* nested schema */ }
}
}
}
The macro generates methods for runtime schema access:
gts_schema_with_refs(): Returns serde_json::Value with $ref in allOfgts_schema_with_refs_as_string(): Returns compact JSON stringgts_schema_with_refs_as_string_pretty(): Returns pretty-printed JSON string// Get schema as JSON value
let schema_value = AuditEventV1::<()>::gts_schema_with_refs();
// Get schema as string (compact or pretty)
let schema_compact = AuditEventV1::<()>::gts_schema_with_refs_as_string();
let schema_pretty = AuditEventV1::<()>::gts_schema_with_refs_as_string_pretty();
// Schema IDs use LazyLock for efficient one-time initialization
let schema_id = AuditEventV1::gts_schema_id();
let parent_id = AuditEventV1::gts_base_schema_id();
The CLI includes security checks:
.json extensionApache-2.0