Architecture
Overview
ReflectAPI has three layers:
- Rust types and handler functions define the API surface.
- Reflection builds a
Schema, which is the interchange format between the server side and code generators. - Codegen backends transform that schema into language-specific clients or an OpenAPI document.
The workspace is split accordingly:
reflectapi-schema: raw schema types and raw-schema transformsreflectapi-schema-codegen: compiler-owned IDs, normalization pipeline, semantic IRreflectapi-derive:#[derive(Input, Output)]macrosreflectapi: reflection traits, builder, runtime integrations, codegen backendsreflectapi-cli: CLI wrapper around codegenreflectapi-demo: snapshot and integration testsreflectapi-python-runtime: runtime support for generated Python clients
Reflection Model
Reflection starts from the Input and Output traits in reflectapi/src/traits.rs. Derived implementations and hand-written impls register types into a Typespace and return TypeReferences that point at those definitions.
The top-level Schema in reflectapi-schema/src/lib.rs contains:
functions: endpoint definitionsinput_types: types seen in request positionsoutput_types: types seen in response positions
Input and output types stay separate at schema-construction time so the same Rust name can have different request and response shapes. Some backends later consolidate them into a single naming domain.
Schema and IDs
SymbolId and SymbolKind live in reflectapi-schema-codegen/src/symbol.rs. They are compiler identifiers, not part of the stable JSON contract.
Key points:
- raw
Schema,Function, and type/member definitions do not store symbol IDs build_schema_ids()inreflectapi-schema-codegen/src/ids.rsassigns IDs in a compiler-owned side table- the schema root now uses
SymbolKind::Schema - the schema root path includes the
__schema__sentinel to avoid colliding with a user-defined type of the same name
That keeps reflectapi.json wire-focused while normalization and semantic analysis still get stable identities.
Type Metadata
Every reflected type is one of:
PrimitiveStructEnum
Primitive.fallback lets a backend substitute a simpler representation when it does not natively model the original Rust type. Examples in the current codebase include pointer-like wrappers falling back to T, and ordered collections falling back to unordered equivalents or vectors.
Language-specific metadata is carried by LanguageSpecificTypeCodegenConfig in reflectapi-schema/src/codegen.rs:
- Rust metadata is serialized when present, for example extra derives on generated Rust types.
- Python type mappings are backend-local in
reflectapi/src/codegen/python.rs, not schema annotations.
Normalization
Normalization lives in reflectapi-schema-codegen/src/normalize.rs.
There are two parts:
- A mutable normalization pipeline over raw
Schema - A
Normalizerthat converts the resulting schema intoSemanticSchema
The configurable pipeline is built with PipelineBuilder. The convenience constructors are:
NormalizationPipeline::standard()Runs type consolidation, naming resolution, and circular dependency resolution.NormalizationPipeline::for_codegen()Skips consolidation and naming, and only runs circular dependency resolution.
After the pipeline runs, Normalizer performs:
- symbol discovery
- type resolution
- dependency analysis
- semantic validation
- semantic IR construction
SemanticSchema provides resolved, deterministic views of functions and types and is defined in reflectapi-schema-codegen/src/semantic.rs.
Backend Behavior
Backends do not all consume the schema in the same way.
TypeScript
The TypeScript backend in reflectapi/src/codegen/typescript.rs consolidates raw schema types and renders directly from the raw schema.
Rust
The Rust backend in reflectapi/src/codegen/rust.rs also works primarily from the raw schema after consolidation.
Python
The Python backend in reflectapi/src/codegen/python.rs uses both representations:
schema.consolidate_types()runs firstvalidate_type_references()checks raw referencesNormalizer::normalize_with_pipeline(...)buildsSemanticSchemausing a pipeline that skips consolidation and naming- rendering uses semantic ordering and symbol information, while still consulting raw schema details where the backend needs original field/type shapes
Python-specific type support is driven by backend-local mappings keyed by canonical Rust type name. Those mappings are static codegen knowledge, not part of the shared schema contract.
OpenAPI
The OpenAPI backend in reflectapi/src/codegen/openapi.rs walks the raw schema directly.
Runtime-Specific Types
ReflectAPI includes special API-facing types whose semantics matter to codegen:
reflectapi::Option<T>: three-state optional value for PATCH-like APIsreflectapi::Empty: explicit empty request/response body typereflectapi::Infallible: explicit “no error payload” type
The Python backend treats these as runtime-provided abstractions rather than generated models.
Testing and Validation
reflectapi-demo is the main regression suite.
The snapshot harness in reflectapi-demo/src/tests/assert.rs generates five artifacts per test:
- raw schema JSON
- TypeScript client output
- Rust client output
- OpenAPI output
- Python client output
The workspace also contains compile-pass and compile-fail tests driven by trybuild.
This architecture chapter is intended to describe the code paths that exist in the repository, not an aspirational future design.