Code generation¶
TypedRest can build clients for you: the code generation tool reads an OpenAPI/Swagger document, infers TypedRest Endpoints from patterns in the described paths and generates source code for a client.
It can generate C#, Java, Kotlin and TypeScript clients.
Two ways to use it¶
- Source generator
- Generates the C# client during compilation, straight from a spec file referenced by your project. Nothing is written to your source tree. Best for .NET projects that build the client alongside the code that consumes it.
- Command-line tool
- Writes the generated code to disk as
.cs,.java,.kotlinor.tsfiles that you check in or post-process.
Both use the same inference, so they infer the same endpoints from the same spec.
How inference works¶
The paths in the document are arranged into a tree, with URI template placeholders such as /contacts/{id} collapsed into a single wildcard node. Each node is then matched against a list of patterns. The first pattern that matches wins:
| Pattern | Matches a path with | Generated endpoint |
|---|---|---|
| Collection | GET returning an array of T, and a child wildcard path with GET returning T |
Collection endpoint |
| Indexer | a child wildcard path, but no matching array response | Indexer endpoint |
| Element | GET with a JSON response body |
Element endpoint |
| Function | POST with both a JSON request and a JSON response body |
Function endpoint |
| Consumer | POST with a JSON request body |
Consumer endpoint |
| Producer | POST with a JSON response body |
Producer endpoint |
| Action | POST with neither |
Action endpoint |
| Blob | GET with a non-JSON response body |
Blob endpoint |
| Upload | POST with a non-JSON request body |
Upload endpoint |
| Default | nothing of the above, but child paths that matched | plain endpoint holding the children |
The whole tree becomes an entry endpoint with the matched endpoints as (nested) properties.
The reactive endpoints (polling, streaming, streaming collection) have no pattern, because an OpenAPI document does not describe the difference between a regular and a streaming response. You can still request them explicitly; see Custom code.
Example¶
Given this excerpt of an OpenAPI document:
paths:
/contacts:
get:
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Contact'
'/contacts/{id}':
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Contact'
'/contacts/{id}/note':
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
'/contacts/{id}/poke':
post:
responses:
'204':
description: ''
'/contacts/{id}/picture':
get:
responses:
'200':
content:
image/jpeg:
schema: { }
/contacts becomes a collection of Contact elements, note an element endpoint, poke an action endpoint and picture a blob endpoint. Generated with the service name Sample, the client is used like this:
var client = new SampleClient(new Uri("https://example.com/api/"));
// GET /contacts
List<Contact> contacts = await client.Contacts.ReadAllAsync();
// GET /contacts/1337/note
Note note = await client.Contacts["1337"].Note.ReadAsync();
// POST /contacts/1337/poke
await client.Contacts["1337"].Poke.InvokeAsync();
SampleClient client = new SampleClient(URI.create("https://example.com/api/"));
// GET /contacts
List<Contact> contacts = client.getContacts().readAll();
// GET /contacts/1337/note
Note note = client.getContacts().get("1337").getNote().read();
// POST /contacts/1337/poke
client.getContacts().get("1337").getPoke().invoke();
val client = SampleClient(URI.create("https://example.com/api/"))
// GET /contacts
val contacts: List<Contact> = client.contacts.readAll()
// GET /contacts/1337/note
val note: Note = client.contacts["1337"].note.read()
// POST /contacts/1337/poke
client.contacts["1337"].poke.invoke()
const client = new SampleClient("https://example.com/api/");
// GET /contacts
const contacts: Contact[] = await client.contacts.readAll();
// GET /contacts/1337/note
const note: Note = await client.contacts.get("1337").note.read();
// POST /contacts/1337/poke
await client.contacts.get("1337").poke.invoke();
Naming¶
Type and property names are derived from the path segments:
- The entry endpoint is named after the service, e.g.
SampleClient. - Collection and indexer endpoints are depluralized, e.g.
/contactsbecomes aContactCollectionEndpointexposed as aContactsproperty. - The element of a collection gets the
Elementsuffix, e.g.ContactElementEndpoint. kebab-case,snake_caseandcamelCasesegments are all converted toPascalCasefor C#, and tocamelCasefor TypeScript, Kotlin and Java.- DTO types are named after the schemas in
components, in a namespace (C#), package (Kotlin/Java) or directory (TypeScript) you can keep separate from the endpoints.
If the same name would be generated in more than one place, the parent name is used as a prefix to disambiguate.
DTO members are an exception in TypeScript: they keep the exact name used on the wire, because TypedRest for TypeScript deserializes with JSON.parse() and so has no way to map a property to a differently named field.
Serializers¶
Generated DTOs have to be annotated for whichever serializer the endpoint uses at runtime, so the tool takes a --serializer option:
| Language | Values | Default |
|---|---|---|
| C# | newtonsoft, system-text-json |
newtonsoft |
| Kotlin | kotlinx, jackson, moshi |
kotlinx |
| Java | jackson, moshi |
jackson |
| TypeScript | (none) |
Each default matches what the corresponding TypedRest port uses out of the box, so most projects never set this.
The choice has to match your runtime configuration. The serializers read entirely different attributes, so a DTO annotated for one silently falls back to its member names under the other, changing the wire format without any error.
Java cannot use kotlinx. kotlinx.serialization generates its serializers with a Kotlin compiler plugin and cannot handle a class written in Java, so the tool rejects the combination.
Overriding the inference¶
For APIs where the patterns do not produce the client you want, you can write the inferred structure into the document as an x-typedrest extension and edit it by hand, or plug in your own patterns and code builders. See Custom code.