Hi everyone ![]()
Crystal’s crystal client generator in OpenAPI Generator has been a beta for a while, and its output never really felt like Crystal — a flat XApi class per tag, prefixed methods, _with_http_info twins, a global singleton client, and several constructs that didn’t even compile (or crashed at runtime) on real-world specs.
I’ve reworked it. PR #24070 — [Crystal] idiomatic API redesign is open and in review for 7.24.0. Sharing it here because feedback from actual Crystal users is exactly what would make it land well.
What the output looks like now
# Before — global singleton, prefixed methods, (value, status, headers) tuples
api = Petstore::PetApi.new
pets, status, headers = api.find_pets_by_status_with_http_info(["available"])
# After — per-instance client, namespaced sub-clients routed from the path
client = Petstore::Client.new(host: "petstore.swagger.io")
pets = client.pet.find_by_status(status: ["available"]).value
And on a real spec, sub-clients nest naturally from the path:
client = Qdrant::Api::Client.new(host: "localhost", scheme: "http")
result = client.collections.points.search(/* ... */).value
What changed
- Namespaced sub-clients routed from the path (
client.collections.points.search) instead ofDcimApi#dcim_cable_terminations_list. - One generic request path —
Connection#request(T) forall T; each operation is a short declarative call returning a typedResponse(T). No_with_http_infoduplicates (~38 lines of boilerplate per op → a handful). - Native multi-instance — every
Clientowns its ownConnection/Configuration. No global state. - Leaner models — one declarative
validatesmacro replaces the per-modelEnumAttributeValidatorhierarchy;==/hashvia stdlibdef_equals_and_hash; a sharedSerializablemixin. - Real-world fixes that the unit fixtures never caught (Crystal only type-checks code it actually reaches):
anyOf/oneOf→ proper union wrappers that try each member (discriminator fast-path when present). The oldoneOfwrapperincludedJSON::Serializablewith no fields and crashed at runtime on deserialise.- Named enums → transparent
alias X = String(the old class withbuild_from_hashcouldn’t be built byJSON::Serializable). allOfinheritance → children stop re-declaring inherited ivars (Crystal forbids re-annotating), forward viasuper(...), and emituse_json_discriminatorfor polymorphism. Modelrequires are topologically ordered.- Multipart file upload, cookie params, non-JSON / binary responses, and opt-in logging — previously broken or missing.
additionalPropertiespreserved across round-trips viaJSON::Serializable::Unmapped.
It’s not a synthetic redesign — it ships in production
The generator powers a three-layer Qdrant stack written in Crystal:
qdrant-api.cr— the low-level Qdrant REST client, generated by this very generator from Qdrant’s full OpenAPI spec (~320 models,anyOfunions, named enums). The newsamples/client/others/crystal-qdrantsample in the PR exercises the same path.qdrant-client.cr— an idiomatic, RAG-oriented wrapper over it (anti-corruption layer — no generated type leaks into its public surface), tested in CI against a real Qdrant container.- mnemodoc — a local-first, self-hosted MCP server (hybrid semantic + keyword doc search for Claude Code / Cursor / …) that runs entirely on your own machine — your docs never leave it. Uses
qdrant-clientas its vector store.
Most of the oneOf/anyOf/named-enum/allOf bugs above were surfaced by generating qdrant-api against that full spec, not by the fixtures.
Status
- petstore sample:
crystal spec→ 92 examples, 0 failures - crystal-qdrant sample:
crystal spec→ 394 examples, 0 failures - codegen unit tests: 38, 0 failures; both samples
crystal tool formatclean
If you generate Crystal clients from OpenAPI specs, I’d love for you to try the branch against your own spec and tell me where the generated API feels awkward — that’s the feedback that’ll make this good. PR is #24070. ![]()