ADR-329: A Reflected Field Also Answers Its Type as meta::Type and Its Documentation — What an API Description Is Built From, Without Annotations

Status: Accepted Date: October 6, 2026 Answers: what a small service needs to describe its routes - an OpenAPI document - from what the program already says, before anything is added to write more Related: ADR-304 D4 (a reflected field answers .name and .of(value)); ADR-323 D1 (meta::Type, a type described as data); ADR-326 (net::serve, http on it); ADR-289 D14 (routes); ADR-251 D2 (documentation is not in the ledger); Part I 1.x (/// is an item’s documentation), Part II 10.3

1. Question

What can a description of an HTTP API - its paths, and for each request and response every field with its type, whether it may be absent, and what it means - be built from in Nikaia?

A shape walk reads a field’s name and value (ADR-304 D4); the compiler checks each unrolled turn with the field’s type, but the program cannot hold that type as a value. meta::Type describes a type as data (ADR-323), and /// before a field is its documentation (Part I), which nothing reads.

2. How others do it

  Types Descriptions Constraints, examples, wire names
Java (springdoc) reflection at run time @Schema(description = …) annotations
C# (Swashbuckle) reflection /// comments and attributes attributes
Rust (utoipa, serde) derive macros doc comments attributes
Python (FastAPI, Pydantic) type hints docstrings, Field(description = …) ordinary values: Field(ge = 0)
Go (swag) - directives in comments directives in comments, unchecked

From memory, not checked against a version. What transfers: the type and the documentation the program already has, read at build time; the route’s own facts as options where the route is made. What is not decided here: syntax for anything more (#496).

3. Options, weighed

Option Why it lost
Annotations first a general construct of the language - for JSON names, validation and mappings too - decided for one use; it has its own record to come
Directives inside /// (Go’s swag) Part I keeps documentation prose the compiler reads no directive from; a directive nobody checks is a mistake nobody sees
The type as text ("String?") the reason ADR-323 gave meta::Type: text is not checked

4. Decision

D1 — A reflected field answers .ty, its type as a meta::Type.

Changed by ADR-330 D5: .ty is the field’s type itself; its kind is field.ty::kind, and the walk goes on with schema(field.ty).

Changes ADR-304 D4: .ty and .doc join .name and .of(value).

fn schema[T: Struct](value: T) -> String {
    let mut out = ""
    for field in T::fields {
        out += f"{field.name}: {json_type(field.ty)}\n"
    }
    return out
}

It is known while the program is built, as .name is: each unrolled turn holds a constant. A field of a struct type is described as Record of that struct’s fields, each described the same way, so a nested request is described whole; a type already being described further up is Named, so a type that holds itself ends; an enum is Named. Nothing about types exists while the program runs but the constant.

D2 — A reflected field answers .doc, its documentation as text.

The /// lines before the field, as written; empty where there are none, and where the build has no source of the package that declares the type - a ledger carries no documentation (ADR-251 D2). It stays prose: nothing is read out of it.

D3 — What belongs to a route, and to the format of a value, is said where it is.

Answered by ADR-331: a wire name, an example and a deprecation are attributes.

A route’s summary and tags are options of the call that makes the route, in the http package. A format - an address, a date - is a type of its own, whose name says it. What has no place yet - a name on the wire other than the field’s, an example, a field marked deprecated - waits for #496.

5. Consequences

6. What this does not decide

This site is open source. Improve this page.