combine-structs
Proc macros for compile-time struct field merging.
Define fields once in separate source structs, then merge them all into a
single flat target struct — no runtime cost, no #[serde(flatten)], and
field access stays direct (target.field, not target.group.field).
Why
Some types are naturally decomposed into logical groups, but consumers
expect a single flat struct. JSON Schema, for example, defines seven
vocabularies (Core, Applicator, Validation, etc.) with ~60 keyword fields
total. We want each vocabulary in its own file with its own docs and
derives, but the final Schema struct should have all fields at the top
level so users write schema.title instead of schema.meta_data.title.
The alternatives have drawbacks:
#[serde(flatten)]— works for serialization, but doubles memory (nested structs), breaks#[derive(Default)]expectations, and doesn't compose well withschemars::JsonSchema.- Copy-paste — fields appear in two places (vocabulary struct + final struct), creating a maintenance burden.
- A single huge file — no logical grouping, hard to navigate.
combine-structs solves this: define fields once per vocabulary struct,
derive Fields, and let #[combine_fields(...)] merge them at compile
time.
How it works
Two proc macros work together via a shared in-memory cache within the compiler process:
-
#[derive(Fields)]on a source struct stores its field definitions (including all attributes, doc comments, and visibility) in the cache. -
#[combine_fields(A, B, C)]on the target struct reads those cached field definitions and emits the target struct with all fields merged in.
The target struct's own fields (defined in its body) are preserved and appear alongside the merged fields.
Usage
use Fields;
use combine_fields;
/// Position fields.
/// Appearance fields.
/// A sprite with position and appearance fields merged in,
/// plus its own `name` field.
// Sprite now has all five fields: x, y, color, visible, name.
let s = Sprite ;
assert_eq!;
assert_eq!;
With serde and schemars
Attributes on source struct fields are preserved through the merge,
so #[serde(rename = ...)] and #[schemars(...)] work as expected:
use ;
use ;
// Schema has: schema, id, and description — all at the top level.
let s = Schema ;
let json = to_value.unwrap;
assert_eq!;
assert_eq!;
Cross-module usage
Source structs can live in any module with no special annotations:
use ;
let s = Sprite ;
assert_eq!;
assert_eq!;
Real-world usage
In Lintel, this crate merges
seven JSON Schema vocabulary structs (~60 fields) into a single flat
Schema type:
Each vocabulary lives in its own module with focused docs and tests,
but the final Schema struct has all fields directly accessible.
License
Apache-2.0