Skip to content

Custom code

For APIs where the pattern inference does not produce the client you want, there are three levels of customization, in increasing order of effort.

1. Annotate the document

When a document contains an x-typedrest extension, both the source generator and the command-line tool use it as-is instead of running the inference. Let the tool write a starting point for you:

typedrest-codegen pattern -f myapi.yml

and then edit the result. The extension is a tree of endpoints, keyed by the property names to generate:

x-typedrest:
  contacts:
    kind: collection
    uri: ./contacts
    description: Collection of contacts.
    schema:
      $ref: '#/components/schemas/Contact'
    element:
      kind: element
      description: A specific contact.
      children:
        note:
          kind: element
          uri: ./note
          schema:
            $ref: '#/components/schemas/Note'
        poke:
          kind: action
          uri: ./poke
        picture:
          kind: blob
          uri: ./picture

Every endpoint takes kind, uri, description and children. Some kinds take additional properties:

kind Additional properties Generated endpoint
(omitted) plain endpoint that only holds children
element schema Element endpoint
collection schema, element Collection endpoint
indexer element Indexer endpoint
action Action endpoint
consumer schema Consumer endpoint
producer schema Producer endpoint
function request-schema, response-schema Function endpoint
blob Blob endpoint
upload form-field Upload endpoint
polling schema Polling endpoint
streaming schema Streaming endpoint
sse schema, event-type SSE endpoint
streaming-collection schema, element Streaming collection endpoint

The reactive kinds are never inferred, since an OpenAPI document does not describe the difference between a regular and a streaming response. Annotating the document is the only way to get them.

Because the generator uses the extension as-is, paths you add to the document later are not picked up automatically. Add them to x-typedrest by hand as well, or remove the extension, rerun pattern and reapply your edits.

2. Extend the generated code

Generated code is overwritten whenever it is regenerated, so do not edit it. Add to it instead. Adding the methods you need for special cases usually beats fighting the generator.

All generated classes and interfaces are partial. Add a file of your own in the same namespace:

namespace MyCompany.MyService;

public partial class ContactElementEndpoint
{
    public Task<Contact?> RenameAsync(string name, CancellationToken cancellationToken = default)
        => UpdateAsync(contact => contact.Name = name, cancellationToken: cancellationToken);
}

If you generate interfaces and want the method on IContactElementEndpoint too, add a partial interface declaration the same way.

The generated entry endpoint constructor takes the base URI and configures the serializer. You can add more constructors next to it in a partial class, for example one that takes an HttpClient for testing:

public partial class MyServiceClient
{
    public MyServiceClient(HttpClient httpClient, Uri uri)
        : base(httpClient, uri, serializer: new SystemTextJsonSerializer())
    {}
}

A partial class cannot change a constructor that has already been generated. If the entry endpoint needs a custom error handler or default headers, turn off its generated constructor (GenerateEntryConstructor="false" for the source generator, --generate-entry-constructor false for the command-line tool) and write your own in a partial class.

Derive from the generated class (the Impl class if you generate interfaces):

public class MyClient extends MyServiceClient {
    public MyClient(URI uri) {
        super(uri);
    }

    public List<Contact> findByName(String name) {
        return getContacts().readAll().stream()
            .filter(x -> name.equals(x.name))
            .collect(Collectors.toList());
    }
}

Generated endpoints are open classes, so you can derive from them (the Impl class if you generate interfaces):

class MyClient(uri: URI) : MyServiceClient(uri) {
    fun findByName(name: String): List<Contact> =
        contacts.readAll().filter { it.name == name }
}

For small additions, an extension function works without subclassing, and on child endpoints too:

fun MyServiceClient.findByName(name: String) = contacts.readAll().filter { it.name == name }

Derive from the generated class:

export class MyClient extends MyServiceClient {
    async findByName(name: string) {
        return (await this.contacts.readAll()).filter(x => x.name === name);
    }
}

When you derive from a generated class, use your subclass in place of the generated one. The generated code still creates child endpoints as the generated types, so subclassing is mostly useful for the entry endpoint. Put helpers for child endpoints on your entry endpoint subclass, or use partial classes (C#) or extension functions (Kotlin).

3. Build your own tool

If you need to change how the inference or the code generation itself works, write your own command-line tool on top of these NuGet packages:

TypedRest.CodeGeneration
Parses OpenAPI/Swagger documents and infers TypedRest Endpoints from patterns. Implement IPattern and add it to a PatternRegistry to recognize your own path shapes.
TypedRest.CodeGeneration.CSharp
Generates C# source code for TypedRest .NET clients.
TypedRest.CodeGeneration.TypeScript
Generates TypeScript source code for TypedRest for TypeScript clients.
TypedRest.CodeGeneration.Jvm
Everything the Kotlin and Java generators have in common: the JVM type model, the naming strategy and the endpoint builders.
TypedRest.CodeGeneration.Kotlin and TypedRest.CodeGeneration.Java
Generate Kotlin and Java source code for TypedRest for the JVM clients.

Implement IBuilder<TEndpoint> and add it to a BuilderRegistry to change the emitted code, or derive from NamingStrategy to change how types and properties are named.

A minimal tool looks like this:

var reader = new OpenApiStreamReader(new OpenApiReaderSettings().AddTypedRest());
var doc = reader.Read(File.OpenRead("myapi.yml"), out _);

foreach (var type in doc.GenerateTypedRest(new GenerationOptions("MyService")
{
    Namespace = "MyCompany.MyService",
    GenerateInterfaces = true,
    GenerateDtos = true
}))
    type.WriteToDirectory("myclient/");

GenerateTypedRest() takes optional PatternRegistry and BuilderRegistry parameters for the extension points above.

For details see the API documentation.