Rust
The Rust SDK is an idiomatic crate emitted from the same GlottoIR as every other target.
Operations under a declared resource hang off an accessor — client.pets().list_pets() — while an
operation you leave ungrouped stays hoisted on impl Client. Request/response shapes are serde
types, so you work with real structs rather than bytes.
Quickstart
cargo add petstore
use futures::StreamExt;
let client = Client::new("https://api.petstore.example")
.with_token(std::env::var("PETSTORE_TOKEN")?);
// a single call
let pet = client.pets().create_pet(NewPet::new("Rex".into())).await?;
// StreamScope owns the paginator until close or scope exit.
let mut stream = StreamScope::new(client.pets().list_pets());
while let Some(pet) = stream.next().await {
println!("{}", pet?.name);
}
stream.close();
Typed models
Each GlottoIR model becomes a serde Deserialize/Serialize type; operations decode and
return the typed response and accept a typed request body. Discriminated unions resolve to the
right enum variant.
Typed errors
Operations return a Result; the Err is an ApiError struct carrying the parsed error body,
plus an ApiErrorKind enum so callers match err.kind() against ApiErrorKind::NotFound rather
than matching status codes. See Errors.
Pagination
Paginated list methods expose a walker that fetches each page as you iterate, advancing the cursor
for you. Wrap the stream in StreamScope and call close() after an early loop break.
The wrapper also releases its inner stream on EOF, a yielded error, or Rust scope exit, including
when consumer code returns through ?. Keeping a bare stream requires explicitly dropping it
when you stop reading. See Pagination.
Manual page companions expose typed items, metadata and explicit next_page() navigation. The lazy response() accessor decodes the full wrapper without another request. See manual pagination.
Retries & backoff
Use RequestOverrides with an operation’s _with_options companion or builder to override headers, timeouts, retry counts and idempotency. Existing RequestOptions values remain accepted. See request options and defaults.
Transient failures (5xx, 429, transport errors) retry with exponential backoff and jitter,
configurable per client. See Retries & timeouts.
Streaming
Server-sent-event endpoints return a typed event stream decoded from the text/event-stream
framing. NDJSON endpoints yield each decoded line through the same stream interface.
Use StreamScope for deterministic cleanup, as with pagination. Stream opening shares the
client retry policy, authentication and hooks. The request timeout ends at the first response
byte; later frames can arrive after that timeout. WebSocket upgrades use the injected HTTP
client and end their opening deadline at the accepted upgrade.
See Streaming and Authentication.
Unknown response fields
Your API can add a response field without it being a breaking change — but a closed struct would drop it, and serde discards what it does not recognize. The generated
models keep it instead: a field the SDK wasn’t generated from is retained on decode, readable
through an accessor, and written back out when the model is re-serialized.
let pet = client.pets().create_pet(body).await?;
// A field your API started returning after this SDK was generated.
if let Some(species) = pet.extra_fields().get("species") {
println!("{species}");
}
// Re-encoding preserves it — a read-modify-write never silently drops it.
let json = serde_json::to_string(&pet)?;
extra_fields() returns an &ExtraFields over serde_json::Values, so nested objects and
arrays survive intact, and retention is recursive.
Constructing a model
Every generated object model has a new(...) constructor taking its required fields.
It initializes optional fields to None and retained fields to an empty bag, so you do not need
to name the internal retention field in a struct literal. A required nullable field still needs
an explicit argument: pass None to send its JSON null.
Models whose caller-supplied fields are all optional also implement Default. This includes
empty models and union variants whose only required field is their generated discriminator.
Use Model::default() for an empty request or Model { field: Some(value), ..Default::default() }
to set selected fields. Referenced models and enum types do not need their own Default because
the optional field itself defaults to None. An optional collection set to Some(Vec::new())
sends an empty array; leaving it None omits the key.
For a decoded model, Model { field: Some(value), ..original.clone() } keeps the other fields’
values and the retained response values. It does not keep an explicit JSON null apart from an
omitted key: an optional field that decoded from null is None, and None is omitted when the
copy is re-encoded, so a PATCH built from it sends that null as an absent key. Set
targets.rust.model_presence: explicit when that distinction
matters; declared fields then use Field<T> (Missing, Null, Value), which changes the model
API. Required-field models and enum types do not implement Default; construct them with their
required arguments or a named enum variant.
To send a field your spec doesn’t model yet, use the per-call extra-body escape hatch rather than the retained bag.
Binary downloads
Binary endpoints return an owned BinaryResponse with response metadata. read_all(max_bytes) bounds delivered bytes and copy_to(&mut sink) writes through AsyncWrite. These helpers consume the owner; close or drop an incremental reader when leaving early. Mixed JSON/binary operations expose an explicit result branch. See file transfers.
File uploads
multipart/form-data operations build the multipart body from their fields for you; an
application/octet-stream operation takes a positional body: Vec<u8> argument sent raw with the
right Content-Type (Option<Vec<u8>> when the body is optional, see below).
Optional request bodies
When your spec marks a request body optional (required: false), the operation takes it as an
Option: body: Option<Model> for a typed body, Option<serde_json::Value> for an inline one and
Option<Vec<u8>> for an octet-stream one. Pass None to send no body and no Content-Type, or
Some(value) to send it. A falsy value is still a value: Some(false), Some(0), Some("") and
Some(Value::Null) are sent as written. An optional multipart/form-data body makes every part an
Option: the parts you pass as Some are the parts sent, and passing None for all of them sends no
body. The same holds on paginated operations, where the body is re-sent unchanged on every later
page. An operation whose config sets auto_populate
body_fields is the one exception to “no body”: it sends just those constants (with their
Content-Type) when you pass None. A required body stays a by-value argument, unless its schema
admits null (see below).
A body whose schema admits null takes one more Option: a $ref marked nullable on the reference
or on the schema it names, or an inline map of any value type marked nullable. The extra layer gives
the explicit JSON null a spelling of its own. A required one is
body: Option<Model>, where None sends null. An optional one is body: Option<Option<Model>>:
the outer None sends no body, Some(None) sends null, and Some(Some(value)) sends the value.
A serde_json::Value body needs no extra layer, since Some(Value::Null) already sends null; neither
does a $ref to a union Glotto emits as a serde_json::Value (a nullable union with no discriminator),
which keeps its Value type and sends null as Value::Null.