openapi-to-rust 0.23.0

Generate typed Rust models, HTTP/SSE clients, and Axum servers from OpenAPI 3.0/3.1 (experimental 3.2)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
# openapi-to-rust — OpenAPI generator for Rust

[![CI](https://github.com/gpu-cli/openapi-to-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/gpu-cli/openapi-to-rust/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/openapi-to-rust.svg)](https://crates.io/crates/openapi-to-rust)
[![docs.rs](https://docs.rs/openapi-to-rust/badge.svg)](https://docs.rs/openapi-to-rust)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

`openapi-to-rust` is an OpenAPI generator for Rust that turns OpenAPI 3.0/3.1 (and experimental 3.2) specifications into strongly-typed structs, async HTTP clients, SSE streaming clients, **and opt-in Axum server scaffolding** — including for the messy, real-world specs everyone actually ships.

Read the [guides and documentation](https://openapi-to-rust.dev/) for the quickest path from an OpenAPI document to compiling Rust.

## 5-second trial

Paste your spec into the **[browser playground](https://openapi-to-rust.dev/playground)** —
the real generator compiled to WebAssembly. See the generated Rust instantly and
download it as a complete, compilable crate. Nothing is uploaded.

## 30-second trial

Install the CLI from crates.io, then generate a tiny client from a stable,
hosted fixture:

```bash
cargo install --locked openapi-to-rust
openapi-to-rust generate https://raw.githubusercontent.com/gpu-cli/openapi-to-rust/v0.8.0/tests/fixtures/operation_extraction/simple_get.json
```

This writes `src/generated/{types,client,mod}.rs` and the exact dependencies
needed to compile them in `src/generated/REQUIRED_DEPS.toml`. Use `--dry-run`
to inspect the plan without writing, and `--check` in CI to detect stale
committed output.

We originally built this internally at [GPU CLI](https://gpu-cli.sh) to generate typed Rust clients for OpenAI, Anthropic, Cloudflare, and other large APIs. After battle-testing it against real-world specs with complex union types, discriminated enums, streaming endpoints, and the occasional spec/API drift, we decided to open source it.

The repository contains **57 real-world specs**. The supported OpenAPI corpus is
56 specs (one Gitea document is Swagger 2.0 and intentionally skipped). Pull
requests compile-check the OpenAI and Anthropic production specs. The full tier
generates all 56 and compile-checks 55; Microsoft Graph is generate-only in CI
because its generated crate exceeds runner memory, and can be force-checked on
a larger local machine.

Release history and breaking changes live in the [changelog](CHANGELOG.md).

## Highlights

- **OpenAPI 3.0 and 3.1, with experimental 3.2 support** — handles `type: ["X", "null"]`, `anyOf`/`oneOf`/`allOf`, discriminated unions, `const`, inline objects, and accepts paths-less specs (components-only or webhooks-only).
- **Generates clients *and* servers** — pick client calls with `[client]`, hosted operations with `[server]`, or keep the default all-operation client. Both share the same `types.rs`.
- **Typed scalars** — `format: date-time` → `chrono::DateTime<chrono::Utc>`, `uri` → `url::Url`, `binary` → `bytes::Bytes`, `uuid` → `uuid::Uuid`, `byte` → `Vec<u8>` + base64 codec, unsigned-int formats → `u32`/`u64`. All opt-out per-format in TOML.
- **Async HTTP client** — typed JSON, text, binary, and multipart operations; bounded responses; retry/backoff via `reqwest-retry`; distributed tracing via `reqwest-tracing`; Bearer / API-key / custom auth; default headers; path-template percent-encoding.
- **Axum 0.8 server scaffolding** — trait per tag, status-code-typed response enum, typed multipart/raw bodies, SSE-ready `OkStream` variant, OpenAPI request validation, and a combined `build_router(...)` factory for multi-tag selections.
- **SSE streaming clients** — raw or typed Server-Sent Events, preserved event metadata, bounded error bodies, reconnect backoff, server `retry:` support, and `Last-Event-ID` resumption.
- **Smart discriminated unions** — auto-detects implicit discriminators from unique `const` properties, and falls back to `#[serde(untagged)]` for ambiguous tags or mixed scalar/object branches (e.g. `"auto"` *or* a tagged object).
- **Per-operation typed errors** — each operation gets its own error enum with `Status4xx(...)` typed bodies; you can match on the exact API error shape.
- **Typed `additionalProperties`** — extra keys become `BTreeMap<String, T>` instead of falling to `serde_json::Value` when the spec gives a value-type schema.
- **Constraints at the server boundary** — `minLength`/`maxLength`/`minimum`/`pattern` remain useful model documentation, and selected Axum operations validate their request schemas at runtime with an offline `jsonschema` bundle.
- **TOML configuration** with overrides for spec quirks (nullable, extensible enums, type aliases).
- **Snapshot testing** — `insta` snapshots for generated output.
- **Optional `specta::Type` derives** for cross-language type sharing.

## Compared with Progenitor

[Progenitor](https://github.com/oxidecomputer/progenitor) is an established,
opinionated OpenAPI 3.0.x client generator developed by Oxide, with particular
alignment to Dropshot-generated APIs. It supports procedural-macro,
build-script, and static-crate workflows, plus pagination streams, generated
CLI support, and typed mocking helpers.

openapi-to-rust grew around consuming large third-party specifications. Its
focus includes OpenAPI 3.1 schema forms, ahead-of-time generated source,
selected Reqwest client operations, SSE, and Axum server scaffolding with shared
models. In one reproducible 120-operation benchmark, its normal ahead-of-time
workflow had 26.5% lower median clean `cargo check` wall time; no-op builds were
effectively identical. This is a version- and machine-specific observation, not
a universal performance guarantee.

See the [dated, source-linked Progenitor comparison](https://openapi-to-rust.dev/compare/progenitor)
and the [benchmark methodology](benchmarks/progenitor-builds/README.md) for the
relevant tradeoffs and limitations.

## Install

Rust users with Rust 1.88 or newer can install the CLI from crates.io:

```bash
cargo install --locked openapi-to-rust
```

This compiles the CLI locally and requires a Rust toolchain. Prebuilt binaries
are not currently published. If you use the generator as a Rust library instead,
run `cargo add openapi-to-rust`.

## Try it in one command

Generate types and an async client directly from a local document or HTTPS URL:

```bash
openapi-to-rust generate openapi.yaml
# Writes src/generated/{types,client,mod}.rs and REQUIRED_DEPS.toml.
```

The async client is the default. Use `--types-only` to omit it, `--dry-run` to
analyze without writing, or `--check` in CI to fail when committed output is
stale. Remote fetches require HTTPS (except loopback development), reject URL
credentials and redirects, and enforce time and response-size limits.

To keep a reusable config:

```bash
openapi-to-rust init openapi.yaml
openapi-to-rust generate
```

## Quick start — client

`openapi-to-rust.toml`:

```toml
[generator]
spec_path = "openapi.json"
output_dir = "src/generated"
module_name = "api"
# All paths above are relative to this TOML file, not the shell's working directory.

[generator.builders]
# Add `*_builder()` when an operation has more than three optional values.
# See "additive operation builders" below for fluent calls and threshold = 0.
enabled = true
threshold = 3

[features]
enable_async_client = true

[http_client]
base_url = "https://api.example.com"
timeout_seconds = 30
max_response_body_bytes = 8388608

[http_client.retry]
max_retries = 3

[http_client.auth]
type = "Bearer"
header_name = "Authorization"
```

Then:

```bash
openapi-to-rust generate --config openapi-to-rust.toml
```

### Select only the client operations you use

`[client]` is optional. Without it—or with an empty `operations` list—the HTTP client keeps every operation for backward compatibility. Selectors share the server grammar: exact `operationId`, exact `METHOD /path`, or `tag:<name>`.

```toml
[client]
operations = [
  "createResponse",
  "GET /v1/models",
  "tag:Files",
]
prune_models = true
```

`prune_models = true` also trims `types.rs`. When client and server selections coexist, the generator retains the union of schemas reachable from both scopes, including inline generated request, response, and parameter types. `[client]` is ignored when `enable_async_client = false`, and it never filters an enabled operation registry.

## Quick start — Axum server

Pick the operations you want to host. The generator emits a trait, a typed response enum, and a router factory. You implement the trait; `axum` does the rest.

```toml
[generator]
spec_path = "openai.yaml"
output_dir = "src/gen"
module_name = "openai"

[features]
enable_async_client = false        # server-only

[server]
framework = "axum"
operations = ["createResponse", "listInputItems"]
# Drop schemas not reachable from the picked operations. Safe when
# you're not also generating the HTTP client (the client would lose types).
prune_models = true

[server.validation]
enabled = true                 # default
max_body_bytes = 2097152       # 2 MiB; 1..=64 MiB
max_errors = 16                # deterministic cap; 1..=100
```

Generated handlers validate JSON bodies and supported path, query, header,
cookie, and form inputs before invoking your trait. Public failures use
`application/problem+json`: malformed transport is `400`, oversized bodies are
`413`, undeclared media types are `415`, and schema violations are `422`.
Messages contain stable codes and JSON Pointer locations, never submitted values,
Serde/validator text, schema paths, or Rust internals. Generated clients preserve
documented typed errors and expose this profile lazily through
`ApiError::problem_details()`.

See [Generated Axum request validation](docs/server-validation.md) for the full
schema dialect, status, error profile, supported-input, and logging contract.

Discover and scaffold operations from the CLI:

```bash
# List operations in the spec (filter by tag / method / substring)
openapi-to-rust server list --tag Responses

# Add an operation to [server].operations (preserves TOML formatting)
openapi-to-rust server add createResponse
openapi-to-rust server add --all-tag Responses
openapi-to-rust server add createResponse --regenerate   # re-run codegen

# Remove
openapi-to-rust server remove createResponse
```

Then implement the trait:

```rust
use gen::server::{ResponsesApi, CreateResponseResponse, build_router, sse_response};
use gen::CreateResponse;
use axum::response::sse::Event;
use futures_util::stream;

#[derive(Clone)]
struct AppState;

#[async_trait::async_trait]
impl ResponsesApi for AppState {
    async fn create_response(&self, body: CreateResponse) -> CreateResponseResponse {
        if body.stream == Some(true) {
            // SSE branch — the generated `sse_response` helper takes any
            // Stream<Item = Result<Event, Infallible>> and returns the
            // exact payload the OkStream variant expects.
            CreateResponseResponse::OkStream(sse_response(stream::iter(vec![
                Ok(Event::default().event("response.created").data("{}")),
                Ok(Event::default().event("response.completed").data("{}")),
            ])))
        } else {
            // Unary branch — typed body, typed response.
            CreateResponseResponse::Ok(/* construct gen::Response */ todo!())
        }
    }
}

#[tokio::main]
async fn main() {
    let app = build_router(AppState);
    let lis = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(lis, app).await.unwrap();
}
```

Two complete examples are in the repo:

- [`examples/server-openai-responses`]examples/server-openai-responses/ — `createResponse` + `listInputItems` + `usage-costs` (multi-tag, body + SSE, four query params, required param 400)
- [`examples/server-anthropic-messages`]examples/server-anthropic-messages/ — `messages_post` with a small overlay that declares `text/event-stream` on the 200 (Anthropic's published spec omits it)

## Generated Output

| File | Description |
|------|-------------|
| `types.rs` | All struct/enum definitions from OpenAPI schemas |
| `client.rs` | Async HTTP client with typed methods per operation (when `enable_async_client`) |
| `sse.rs` | Reusable SSE transport, framing, typed/raw decoding, and reconnection support (when SSE is enabled) |
| `streaming.rs` | Endpoint-specific SSE request methods backed by `sse.rs` (when configured) |
| `server/mod.rs` | Module re-exports for the server (when `[server]` is set) |
| `server/api.rs` | `trait <Tag>Api { async fn <op>(&self, …) -> <Op>Response; }` per tag |
| `server/errors.rs` | Status-typed response enums with JSON, bodyless, wildcard/default, and status-correct SSE variants plus `IntoResponse` |
| `server/router.rs` | Per-tag `Router` factory; combined `build_router<…>(…)` for multi-tag selections |
| `server/validation.rs` | Offline JSON Schema validators and sanitized Problem Details rejections |
| `mod.rs` | Module declarations + re-exports |
| `REQUIRED_DEPS.toml` | Complete direct dependencies and crate features for the exact generated modes and selected operations — append or merge into the consuming `Cargo.toml` |
| `bindings.json` | Optional versioned description of the emitted Rust symbols, signatures, and source operations |
| Configured effective-spec file | Optional transformed OpenAPI JSON document used for generation |

### Generated client usage

```rust
use crate::generated::client::HttpClient;
use crate::generated::types::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = HttpClient::new()
        .with_base_url("https://api.example.com")
        .with_max_response_body_bytes(8 * 1024 * 1024)
        .with_api_key(std::env::var("API_KEY")?);

    let req = CreateResourceRequest { /* … */ };
    let resource = client.create_resource(req).await?;
    Ok(())
}
```

Generated clients cap every response they buffer in memory at 8 MiB by
default. Set `http_client.max_response_body_bytes` when generating, or use the
runtime builder above for a particular client. If a cumulative chunked or
fixed-length body crosses the limit, the call returns
`HttpError::ResponseTooLarge { limit }` before appending the chunk that would
exceed it. Successful SSE responses remain streaming; only SSE error responses
are buffered under the same cap.

### Custom HTTP transport

Supply a preconfigured `reqwest::Client` to customize TLS, trusted roots, proxies,
timeouts, or default headers for both HTTP and SSE requests:

```rust
let transport = reqwest::Client::builder()
    .timeout(std::time::Duration::from_secs(30))
    .build()?;
let client = HttpClient::with_client(transport)
    .with_base_url("https://api.example.com");
```

For mutual TLS, configure `.identity(identity)` and, when needed,
`.add_root_certificate(certificate)` on the reqwest builder before calling
`build()`. Enable the appropriate TLS features in your consuming crate.

`with_client` uses the same middleware defaults as `new()` (retry disabled,
tracing enabled when generated). When retry or tracing is generated,
`with_client_and_config` accepts the transport followed by the same arguments
as `with_config`. With both enabled, for example:

```rust
let client = HttpClient::with_client_and_config(
    transport,
    Some(RetryConfig::default()),
    false, // disable tracing
);
```

Existing `new()`, `Default`, and `with_config(...)` calls keep their signatures
and behavior. These constructors do not automatically configure mutual TLS
from an OpenAPI security scheme.

### Multipart files

A file field of a typed multipart body (`format: binary`, or a union of text
and bytes) is a `MultipartFile`: the part's content, in the `types.binary`
carrier, with the filename and content type to send it under. It converts from
its content, so a plain value sends a part with neither:

```rust
let request = CreateUploadRequest {
    document: MultipartFile::from(pdf_bytes)
        .with_file_name("report.pdf")
        .with_content_type("application/pdf"),
    attachment: Some(notes.into()),
    ..
};
client.create_upload(request).await?;
```

A file's own content type wins over a single media type declared in
`encoding.<field>.contentType`, which applies otherwise. A list of declared
types, or a range such as `image/*`, sets none. An array field is one part per
item under the field's name, so `Vec<MultipartFile>` sends several files, each
with its own filename. An object field is one JSON part, typed
`application/json` unless `encoding` names another type.

### Wildcard request bodies

An operation whose request body declares only media ranges such as `image/*`
or `*/*` needs a concrete `Content-Type` for each upload. Its generated
`*_with_content_type()` method takes the existing arguments followed by a media
type. For example, an `uploadImage` operation with an optional body and no other
arguments supports:

```rust
client
    .upload_image_with_content_type(Some(image_bytes), "image/png")
    .await?;
```

The media type must be valid, concrete, and match at least one declared range.
Parameters such as `charset=utf-8` are preserved. The per-call value overrides
configured `Content-Type` headers. When an optional body is `None`, the value is
ignored. The original method still reports a configuration error if a body is
present without a concrete type.

With [operation builders](#generatorbuilders--additive-operation-builders)
enabled, the same upload can be written as:

```rust
client
    .upload_image_builder()
    .body(image_bytes)
    .content_type("image/png")
    .send()
    .await?;
```

The content-type setter counts as an optional value for builder generation;
use `threshold = 0` to include a small upload operation like this one.

### Response representations and live downloads

An operation declaring JSON, binary, and SSE retains its existing preferred
method and gains methods for the other representations. For a `render` operation
whose preferred response is JSON, examples include `render_binary()` for buffered
bytes and `render_binary_stream()` for live chunks. With SSE client generation
enabled, `render_sse()` yields parsed `SseEvent<String>` values through the
existing SSE parser; otherwise `render_event_stream()` exposes raw chunks.
Existing configured typed SSE and reconnection APIs remain available.
The standalone library helpers `generate_http_client` and
`generate_operation_methods` expose raw SSE streams so their output remains
self-contained with the generated models. `generate_all` includes the parsed
SSE runtime when enabled.

```rust
use futures_util::StreamExt;

let stream = client.render_binary_stream().await?;
futures_util::pin_mut!(stream);
while let Some(chunk) = stream.next().await {
    let bytes = chunk?;
    // Write or process this chunk before the download finishes.
}
```

New representation methods set their declared `Accept` value and validate the
response representation before decoding. Successful live streams do not buffer
the complete body or apply the buffered body's total byte ceiling; error bodies
remain bounded. If a media type has different schemas at different successful
statuses, generated status-specific methods accept only their documented status
and return an inspectable error for another status. A method cannot control which
status a server returns.

Method names are allocated around real operations and builders. Multiple media
types of the same kind include a normalized media-type suffix. Consult generated
Rust or the binding metadata for the exact allocated names. Filename overrides
also compose with multipart operations' response variants.

Generated HTTP clients compile for `wasm32-unknown-unknown` as well as native
targets, including the opt-in SSE runtime (`enable_sse_client`, with optional
`[[streaming.endpoints]]`) and the opt-in retry middleware
(`[http_client.retry]`). On wasm32 the generated SSE stream is single-threaded
(no `Send` bound) because the browser `fetch` body is not `Send`; native
streams keep `Send`. The emitted dependency fragment scopes the wasm-only
`futures-timer/wasm-bindgen` and `getrandom/wasm_js` features to
`cfg(target_arch = "wasm32")`, so the native dependency set is unchanged.

## What the generated types look like

A tour of patterns the generator emits, from real outputs.

### Typed scalars

```rust
// format: date-time → chrono::DateTime<chrono::Utc>
pub created_at: chrono::DateTime<chrono::Utc>,
pub archived_at: Option<chrono::DateTime<chrono::Utc>>,

// format: uri → url::Url
pub url: url::Url,
pub callback_url: Option<url::Url>,

// format: binary → bytes::Bytes (a multipart file field is a
// `MultipartFile` holding it, with its filename and content type)
Binary(bytes::Bytes),

// format: uuid → uuid::Uuid
pub request_id: uuid::Uuid,
```

### Typed `additionalProperties`

```rust
pub additional_properties: std::collections::BTreeMap<String, f64>,    // usage maps
pub additional_properties: std::collections::BTreeMap<String, String>, // labels
```

### Constraints as doc comments

```rust
///Constraint: minLength=1, maxLength=64, pattern=`^[a-zA-Z0-9_-]{1,64}$`
pub custom_id: String,

///Constraint: minimum=0, maximum=1
pub temperature: Option<f64>,
```

> Generated model types do not grow `validator`/`garde` derives or perform
> validation during construction. When Axum server generation is enabled,
> request constraints are instead enforced once at the HTTP boundary from the
> original OpenAPI/JSON Schema source of truth.

### Discriminated unions (tagged enums)

```rust
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageContent {
    Text(TextContent),
    Image(ImageContent),
}
```

### Hybrid string-or-object unions

When an `anyOf`/`oneOf` mixes a string-enum branch with tagged-object branches (a common OpenAI pattern), the generator emits an **untagged** enum so both forms deserialize:

```rust
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]                                   // not #[serde(tag="type")]
pub enum ToolChoiceParam {
    ToolChoiceOptions(ToolChoiceOptions),            // string-enum: "none"|"auto"|"required"
    ToolChoiceFunction(ToolChoiceFunction),
    ToolChoiceMCP(ToolChoiceMCP),
    // …
}
```

### Extensible enums (with `Custom(String)` fallback)

When the spec declares an `anyOf` of `const` strings plus an open `string` branch (or you opt in via `[extensible_enums]`, see below), the enum has a `Custom(String)` arm so unknown values still deserialize:

```rust
pub enum Model {
    ClaudeSonnet46,
    ClaudeOpus46,
    ClaudeHaiku45,
    Claude3Haiku20240307,
    Custom(String),         // ← anything not in the known set
}
```

### Per-operation typed errors

Each operation has its own error enum that wraps the typed body of each documented response code:

```rust
let resp = client.create_response(req).await;
match resp {
    Ok(body) => { /* … */ }
    Err(ApiOpError::Api(err)) => match err.typed {
        Some(CreateResponseApiError::Status4xx(typed)) => {
            // typed is the spec's typed 4xx body, e.g. ResponseInfo {
            //   code: 10042,
            //   message: "Please enable R2 through the Cloudflare Dashboard.",
            // }
        }
        _ => eprintln!("raw body: {}", err.body),
    },
    Err(ApiOpError::Transport(e)) => eprintln!("transport: {}", e),
}
```

## Streaming (SSE)

SSE support has two layers: `StreamingConfig` selects and types operations at
generation time, while generated `sse.rs` owns the transport and parser. The
transport is also usable directly for APIs whose event payloads are only known
at runtime.

```rust
use openapi_to_rust::streaming::*;

let streaming_config = StreamingConfig {
    endpoints: vec![StreamingEndpoint {
        operation_id: "createChatCompletion".to_string(),
        path: "chat/completions".to_string(),
        stream_parameter: "stream".to_string(),
        event_union_type: "ChatCompletionStreamEvent".to_string(),
        event_flow: EventFlow::StartDeltaStop {
            start_events: vec!["response.created".to_string()],
            delta_events: vec!["response.output_text.delta".to_string()],
            stop_events: vec!["response.completed".to_string()],
        },
        ..Default::default()
    }],
    reconnection_config: Some(ReconnectionConfig {
        max_retries: 5,
        initial_delay_ms: 500,
        max_delay_ms: 16000,
        backoff_multiplier: 2.0,
    }),
    ..Default::default()
};
```

Generated event types are tagged enums you can match on directly:

```rust
use futures_util::StreamExt;
use generated::sse::{SseClient, SseReconnectOptions};
use generated::types::ResponseStreamEvent;

let client = SseClient::new()
    .with_reconnect_options(SseReconnectOptions::default());
let request = client
    .post("https://api.example.com/v1/responses")
    .bearer_auth(api_key)
    .json(&request_body);
let mut events = client
    .stream_json_reconnecting::<ResponseStreamEvent>(request)
    .await?;

while let Some(event) = events.next().await {
    let event = event?;
    // event.event, event.id, and event.retry preserve SSE transport metadata.
    match event.data {
        ResponseStreamEvent::TextDelta(d) => out.push_str(&d.delta),
        ResponseStreamEvent::Completed(_) => break,
        _ => {}
    }
}
```

Reconnecting streams honor a server-provided `retry:` delay and send the latest
non-empty event ID as `Last-Event-ID`. Use `stream_raw` or
`stream_raw_reconnecting` when payloads are not JSON; `stream` retains the
payload-only typed interface for callers that do not need transport metadata.

The generator also **auto-detects** streaming endpoints: any response declaring `content: text/event-stream` flips `supports_streaming = true` automatically. Explicit `[[streaming.endpoints]]` config still wins when present.

## Additional model derives

Add traits to generated types without editing the generated files:

```toml
[generator.derives]
string_enums = ["PartialOrd", "Ord"]

[generator.derives.types]
DayOfWeek = ["Hash"]
Coordinates = ["PartialEq", "Eq", "PartialOrd", "Ord", "Hash"]
```

`string_enums` covers closed and extensible string enums, including inline
property/array-item enums and HTTP parameter enums. Per-type entries use the exact
emitted Rust name: a schema called `day-of-week` is selected as `DayOfWeek`.
They also support model structs and payload enums. Group and per-type additions
combine with existing derives; duplicate standard traits are emitted once.
Both settings default to empty, preserving existing generated output.

Derived enum ordering follows variant declaration order, preserving the order of
values in the spec. Extensible enums place `Custom(String)` after known variants
and compare custom strings lexicographically. Reordering spec values therefore
changes ordering after regeneration. Serialization remains unchanged.

Derives do not propagate to referenced models or add prerequisite traits:
request `PartialEq`, `Eq`, `PartialOrd`, and `Ord` together for a struct that needs
ordering. All fields and payloads must support the requested traits; for example,
`f64` cannot derive `Eq` or `Ord`. Rust checks these bounds in the consuming crate.
Qualified custom derive paths such as `my_crate::MyDerive` are supported; add their
dependencies yourself alongside `REQUIRED_DEPS.toml`. Custom macro dependencies
are not inferred.

Malformed paths, unknown or ambiguous type names, explicit type-alias targets,
and conflicts with known manual trait implementations are generation errors.
Existing Specta derives keep their feature gate. Valid targets omitted by
operation selection or model pruning are ignored. Runtime helpers, operation
builders, client error enums, and server response enums are outside this policy.

Library consumers can set `GeneratorConfig::derives` with the exported
`DerivesConfig`. The policy applies to `generate`, `generate_all`,
`generate_all_with_bindings`, and `generate_http_client`.

## Spec-quirk overrides

Real specs lie. These TOML knobs let you patch quirks without forking the spec.

### `schema_extensions` — overlay JSON/YAML fragments onto the spec

A list of files whose top-level objects are deep-merged into the main spec before analysis. Use it to add a `text/event-stream` content entry, add a missing operation, or tweak a schema without forking the upstream spec. Example: the Anthropic server example overlays `sse-overlay.json` so `messages_post` gets an SSE response variant.

```toml
[generator]
# Relative paths are resolved from the directory containing this TOML file.
schema_extensions = ["sse-overlay.json"]
```

### OpenAPI Overlay 1.1 and the effective document

Standard [OpenAPI Overlay 1.1](https://spec.openapis.org/overlay/v1.1.0.html)
documents are configured separately from the existing deep-merge fragments:

```toml
[generator]
overlays = ["remove-legacy.overlay.yaml", "customize.overlay.json"]
effective_spec = "effective-openapi.json"
```

Input paths resolve relative to the configuration file. Processing applies
`schema_extensions` first, then overlays and their actions in order, then
validates and analyzes the resulting document. Server defaults also come from
that effective document. Existing extension merge behavior is preserved.
The optional effective-spec artifact is deterministic JSON inside `output_dir`;
it describes the same document used for generation, before Rust name allocation.

Overlay actions use RFC 9535 JSONPath for `update`, `copy`, and `remove`.
Errors identify the overlay file and action. An overlay's `extends` identifies
its intended source; it does not implicitly fetch or replace the configured
input. Direct-mode equivalents are repeatable `--overlay <path>` and
`--effective-spec <filename>`. `--check` verifies the effective artifact along
with Rust output, and `--dry-run` writes nothing.

Library callers can use `overlay::preprocess_spec(spec, &extensions, &overlays)`
before constructing a `SchemaAnalyzer`.

### Binding metadata

Enable a deterministic JSON artifact describing the Rust API actually emitted:

```toml
[generator]
bindings_metadata = "bindings.json"
```

The CLI also accepts `--bindings-metadata bindings.json`. Version 1 records
module and re-export paths, structs and their fields/wire names, enum payloads,
aliases, public helpers, builders, and exact HTTP-client method signatures.
Operation methods retain source JSON Pointer locations and explicit response
media/status/consumption descriptors even when their Rust names are disambiguated.
Consumers should check `schema_version` and `coverage` before interpreting the
artifact. It contains no generation timestamp or machine-specific input paths.

Metadata is collected from the same emission AST and client plans used to render
source, and is separate from the client-sync manifest. Its initial coverage is
models and HTTP clients, including their parsed SSE helpers; configured dedicated
streaming clients, servers, and registries are rejected when metadata is requested.
Without metadata enabled, those generation modes remain available as usual.
The sidecar participates in `--check` and `--dry-run`.

The library API is additive and preserves `GenerationResult`:

```rust
let output = generator.generate_all_with_bindings(&mut analysis)?;
generator.write_files(&output.generation)?;
let metadata_json = output.bindings.to_json()?;
```

Configured sidecar filenames must be distinct output-relative filenames and
cannot replace generated Rust modules or dependency fragments.

### `nullable_overrides` — force a field to `Option<T>`

When a spec marks a field as required + non-nullable but the API actually returns `null`. Format: `"SchemaName.fieldName" = true`.

```toml
[nullable_overrides]
# OpenAI's spec uses a bare $ref for `error`; the API actually returns null on success.
"Response.error" = true
```

### `extensible_enums` — force a closed enum to accept unknown values

When the spec declares a fixed enum but the API actually returns values outside the set (real-world drift). Renders the enum with a `Custom(String)` fallback variant. Accepts either the raw spec name or the rendered Rust type name.

```toml
[extensible_enums]
# CF spec declares lowercase ["apac", ..., "wnam"] but the API returns "WNAM".
"R2BucketLocation" = true
# OpenAI spec declares ["in-memory", "24h"] but the API returns "in_memory".
"ModelResponsePropertiesPromptCacheRetention" = true
```

### `type_mappings` — override a primitive's Rust type

```toml
[type_mappings]
"DateTime" = "chrono::DateTime<chrono::Utc>"
```

### `[generator.types]` — typed-scalar strategy per format

Opt out of any individual typed scalar (e.g. fall back to `String` for date-times if you don't want `chrono`):

```toml
[generator.types]
date_time = "string"        # default: "chrono"
uri       = "string"        # default: "url"
binary    = "string"        # default: "bytes"
```

The CLI also supports `--types-conservative`, which collapses every typed scalar to `String`/`i64`/etc. Use it when you want zero optional-crate dependencies.

### `[generator.builders]` — additive operation builders

Operation builders are opt-in methods on the generated `HttpClient` in
`client.rs`. Enable them in `openapi-to-rust.toml` and regenerate:

```toml
[generator.builders]
enabled = true
threshold = 3
```

```bash
openapi-to-rust generate --config openapi-to-rust.toml
```

A builder is emitted only when its optional parameters, optional body,
eligible optional request-body fields, and content-type setter for wildcard
bodies exceed `threshold`. The default threshold is `3`, so four optional values qualify.
Set `threshold = 0` to include operations with at least one optional value;
operations with no optional values do not gain builders.

Required operation parameters and required request-model values are arguments to
`*_builder(...)`. Optional values become named setters. For example, an
`updateWidget` operation with required widget ID, tenant ID, and name can use:

```rust
let widget = client
    .update_widget_builder("widget-1", "tenant-1", "New name".to_string())
    .search("needle")
    .limit(20)
    .send()
    .await?;
```

Setters own their values; `.send().await` uses the same serialization and error
handling as the flat operation method. For wildcard request bodies, add
`.content_type("image/png")` alongside `.body(bytes)` as shown
[above](#wildcard-request-bodies). Existing flat async methods remain available.
Builder generation is disabled by default.

When using the Rust library:

```rust
use openapi_to_rust::{GeneratorConfig, config::BuildersSection};

let config = GeneratorConfig {
    builders: BuildersSection { enabled: true, threshold: 0 },
    ..Default::default()
};
```

## OpenAPI 3.1 / 3.2 support

The generator accepts 3.0.x and 3.1.x specs; 3.2.x parses with an
`experimental` warning. Unknown fields are retained for compatibility, but an
unrecognized field may be ignored by analysis and code generation. Add a
focused fixture when relying on a less-common OpenAPI or JSON Schema keyword.

Path parameters with `allowReserved: true` use OpenAPI 3.2 reserved expansion:
reserved characters such as `:` and existing `%XX` triples pass through, while
`/`, `?`, and `#` remain percent-encoded. The generator also accepts this path
behavior in 3.0.x and 3.1.x specs as a compatibility extension; those versions
define `allowReserved` only for query parameters. Omitting the flag or setting
it to `false` preserves the default path encoding. Query `allowReserved` is
currently parsed but does not change generated query serialization.

**3.1 (JSON Schema 2020-12) keywords now modeled and read from typed fields:**

| Keyword group | Status |
|---|---|
| `type` as array (e.g. `["string", "null"]`) | typed + used for nullability |
| `prefixItems`, `unevaluatedItems`, `contains` / `minContains` / `maxContains` | typed |
| draft-04 positional `items: [A, B]` (FastAPI/pydantic v1 emits it under 3.1) | typed as `prefixItems` |
| Fixed-length `prefixItems` | generated as Rust tuples, e.g. `(String, i64)` |
| `patternProperties`, `propertyNames`, `unevaluatedProperties` | typed |
| `dependentRequired`, `dependentSchemas`, `if` / `then` / `else` | typed |
| `contentEncoding`, `contentMediaType`, `contentSchema` | typed |
| `$dynamicRef`, `$dynamicAnchor`, `$defs`, `$id`, `$schema`, `$comment` | typed (anchor-scope resolution is a follow-up) |
| Path Item `$ref` resolution | resolved at analysis time |
| Webhooks (`webhooks:`) | ingested as operations |
| `examples`, `example`, `title`, `deprecated`, `readOnly`/`writeOnly` | typed |

**3.2 deltas (experimental):**

| Delta | Status |
|---|---|
| Path parameter `allowReserved` | emits reserved expansion; also supported as an extension in 3.0/3.1 |
| `query` HTTP method + `PathItem.additionalOperations` | parses + emits client methods via `reqwest::Method::from_bytes(...)` |
| OAuth `deviceAuthorization` flow + `oauth2MetadataUrl` | typed |
| `Server.name`, `Tag.parent`/`kind`/`summary` | typed |
| `Discriminator.defaultMapping` | typed (captured; `_Other(Value)` fallback emission is a follow-up) |
| `MediaType.itemSchema`, `prefixEncoding`, `itemEncoding` | typed |
| `mediaTypes` in Components | typed |

**Modeled Components family:** `Server`, `ServerVariable`, `SecurityScheme` (apiKey / http / mutualTLS / oauth2 / openIdConnect with all flows), `OAuthFlows`, `Encoding`, `Header`, `Example`, `Link`, `Callback`, `Tag`, `ExternalDocs`, `Discriminator`. Modeling does not imply runtime support for every security or linking behavior; for example, mutual TLS credentials are not configured by generated clients.

The fixture harness under `tests/conformance/` currently gates lossless L0
parsing; its later L1–L5 markers are tracked but deferred. The vendored JSON
Schema Test Suite runner enforces a no-regression ceiling for parse failures
and lossless round trips; it does not yet enforce semantic, feature-level pass
thresholds.

## CLI

```bash
# Direct mode: client by default; use --types-only to omit it.
openapi-to-rust generate openapi.yaml
openapi-to-rust generate https://example.com/openapi.json --dry-run --json
openapi-to-rust generate openapi.yaml --check --quiet

# Create a starter config, then generate with the config defaults.
openapi-to-rust init openapi.yaml
openapi-to-rust generate

# Explicit config mode.
openapi-to-rust generate --config openapi-to-rust.toml
openapi-to-rust generate --config openapi-to-rust.toml --types-conservative
openapi-to-rust validate --config openapi-to-rust.toml

# Server scope management — preserves TOML formatting (toml_edit)
openapi-to-rust server list                    # all operations
openapi-to-rust server list --tag Responses    # filter by tag
openapi-to-rust server list --method POST --grep response
openapi-to-rust server list --json             # JSON output
openapi-to-rust server add createResponse                 # add one
openapi-to-rust server add --all-tag Responses            # expand a tag
openapi-to-rust server add createResponse --dry-run       # preview
openapi-to-rust server add createResponse --regenerate    # add + regenerate
openapi-to-rust server remove createResponse              # remove
```

Selectors are forgiving — `operationId`, `METHOD /path`, or `tag:<name>`. Typos surface Levenshtein-based "Did you mean …?" suggestions.

## TOML reference

```toml
[generator]
spec_path = "openapi.json"              # required
output_dir = "src/generated"            # required
module_name = "types"                   # informational label, not a directory
schema_extensions = []                  # optional list of JSON/YAML overlays merged into the spec

[features]
enable_sse_client = false               # parsed SSE variants; optional [[streaming.endpoints]] adds typed clients
enable_async_client = true              # generate HTTP REST client
enable_specta = false                   # add specta::Type derives
enable_registry = false                 # generate static operation registry (CLI/proxy routing)
registry_only = false                   # only generate the registry (skip types/client/streaming)

[http_client]
base_url = "https://api.example.com"
timeout_seconds = 30                    # 1-3600
max_response_body_bytes = 8388608       # buffered responses; 8 MiB default

[http_client.retry]
max_retries = 3                         # 0-10
initial_delay_ms = 500                  # 100-10000
max_delay_ms = 16000                    # 1000-300000

[http_client.tracing]
enabled = true

[http_client.auth]
type = "Bearer"                         # Bearer | ApiKey | Custom (honored at runtime)
header_name = "Authorization"

[[http_client.headers]]
name  = "content-type"
value = "application/json"

[[streaming.endpoints]]
operation_id     = "createChatCompletion"
path             = "chat/completions"
http_method      = "POST"
stream_parameter = "stream"
event_union_type = "ChatCompletionStreamEvent"
content_type     = "text/event-stream"

[streaming.endpoints.event_flow]
type          = "StartDeltaStop"        # or "Continuous"
start_events  = ["response.created"]
delta_events  = ["response.output_text.delta"]
stop_events   = ["response.completed"]

[server]
framework  = "axum"                     # only axum supported today
operations = [                          # selectors: operationId | "METHOD /path" | "tag:<name>"
  "createResponse",
  "POST /v1/messages",
  "tag:Responses",
]
prune_models = false                    # drop schemas outside the combined selected
                                        # client/server operation closure

[server.validation]
enabled = true
max_body_bytes = 2097152
max_errors = 16

[client]
operations = [                          # absent or empty means every client operation
  "createResponse",
  "GET /v1/models",
  "tag:Files",
]
prune_models = false                    # opt in to the same union model pruning

[nullable_overrides]
"Response.error" = true                 # see "Spec-quirk overrides" above

[extensible_enums]
"R2BucketLocation" = true               # see "Spec-quirk overrides" above

[type_mappings]
"DateTime" = "chrono::DateTime<chrono::Utc>"

[generator.types]
date_time = "chrono"                     # chrono (default) | time | string
uri       = "url"                        # url     (default) | string
binary    = "bytes"                      # bytes   (default) | vec_u8 | string
uuid      = "uuid"                       # uuid    (default) | string
byte      = "base64"                     # base64 (default) | base64_url_unpadded | vec_u8 | string
unsigned  = true                         # uint32/uint64 -> u32/u64
float_precision = "f64"                  # f64 (default) | f32 — see below

[generator.types.shape]
additional_properties_typed = true
```

**`format: float` maps to `f64`, not `f32`.** JSON carries no binary32, so the
declared format describes the server's storage rather than the transport. A
price sent on the wire as `0.03` parses losslessly into `f64`, but through
`f32` it becomes `0.029999999329447746` — a real hazard when the field is
money. Set `float_precision = "f32"` to map strictly by declared format.

## Testing

```bash
cargo test                # unit + integration tests
cargo insta test          # snapshot tests
cargo insta review        # review snapshot diffs
scripts/spec-compile.sh   # generate + cargo-check every spec in specs/ (full corpus)
```

`scripts/spec-compile.sh` also synthesizes valid JSON instances from each
source component schema, hydrates the exact generated Rust model, serializes it
back to JSON, validates that output against the same schema, and requires a
stable second round trip. The summary reports tested components, generated
samples, and explicit skips. Pass one or more spec names (for example,
`scripts/spec-compile.sh anthropic`) for a focused trial before running the
full 55-spec compile suite.

The compile tiers are intentionally different:

- Every pull request and push to `main` generates all 55 supported OpenAPI
  specs, then compile-checks the Anthropic and OpenAI production specs against
  each generated `REQUIRED_DEPS.toml`.
- Weekly scheduled CI and manual workflow runs generate all 55 supported
  OpenAPI specs and compile-check 54. Microsoft Graph is reported as
  generate-only because its generated crate exceeds the 16 GB runner; the
  bundled Gitea Swagger 2.0 document is reported as skipped.
- Local `scripts/spec-compile.sh` runs the same full tier; pass spec names as
  arguments for a smaller targeted run. On a machine with sufficient memory,
  `SPEC_COMPILE_FORCE_CHECK=1 scripts/spec-compile.sh microsoft-graph` also
  compile-checks Microsoft Graph (about 19 GB peak RSS on the current corpus).
- Every pull request also starts the generated OpenAI Responses and Anthropic
  Messages Axum examples on loopback and exercises them through pinned official
  Python SDKs. The compatibility gates cover unary and streaming responses,
  with the OpenAI row also covering input-item path/query parameters and the
  organization costs query.

## Examples

```bash
# Library / generator examples
cargo run --example basic_generation
cargo run --example client_generation_example
cargo run --example discriminated_unions
cargo run --example anyof_unions
cargo run --example allof_composition
cargo run --example openai_patterns
cargo run --example toml_config_example

# End-to-end Axum server examples (generate + run)
cargo run -p openapi-to-rust -- generate --config examples/server-openai-responses/openapi-to-rust.toml
cargo run --manifest-path examples/server-openai-responses/Cargo.toml

cargo run -p openapi-to-rust -- generate --config examples/server-anthropic-messages/openapi-to-rust.toml
cargo run --manifest-path examples/server-anthropic-messages/Cargo.toml
```

## Versioning

Until 1.0.0, a minor version may change generated Rust APIs when correcting
output that was wrong or incomplete on the wire. Regenerating lets the compiler
identify affected call sites. See the [changelog](CHANGELOG.md) for release
history and migration notes.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, targeted test commands,
snapshot guidance, and the generated-API compatibility checklist. Questions
belong in [GitHub Discussions](https://github.com/gpu-cli/openapi-to-rust/discussions);
bugs and concrete feature requests use the repository issue forms.

By participating, you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
Security reports follow [SECURITY.md](SECURITY.md); please do not file a public
issue for a suspected vulnerability.

## License

MIT