Reference

Table of Contents

Generating only a subset of the API

Sometimes you may receive a huge OpenAPI specification file and you are only interested in a subset of it. The plugin allows you to specify which methods you would like to include per execution:

<execution>
    ...
    <configuration>
        ...
        <!-- If this is specified, only the listed endpoints will be included -->
        <endpoints>
            <!-- Include all methods of the /sum endpoint -->
            <endpoint>/sum</endpoint>
            <!-- Include only the GET method of the /user/{id} endpoint -->
            <endpoint>/user/{id}:get</endpoint>
            <!-- Include only the POST and PUT methods of the /user endpoint -->
            <endpoint>/user:post,put</endpoint>
        </endpoints>
    </configuration>
</execution>

Splitting a large API into multiple files

When you have a large API you may want to split the generated code into multiple files so you don’t end up with a monster delegate. The plugin allows you to use OpenAPI tags to split the generated code into multiple files:

<execution>
    ...
    <configuration>
        ...
        <interfaceName>MyInterface</interfaceName>
        <!-- If true, the generated code will be split into multiple files based on the OpenAPI tags -->
        <splitByTags>true</splitByTags>
        ...
    </configuration>
</execution>

Now you can use OpenAPI tags to split the generated code into multiple files:

...
paths:
  /foo:
    get:
      ...
      tags:
        - foo
  /bar:
    post:
      ...
      tags:
        - bar

This will generate two interfaces: MyInterfaceFooDelegate and MyInterfaceBarDelegate which you can implement in your application. If a method does not have a tag, a third interface MyInterfaceDelegate will be generated which contains all the methods without a tag. If a method has multiple tags, it will be included in the corresponding interface of the first tag.

Additional validation checks for values and models

OpenAPI already defines a few validation rules like value type (number, string, object, etc.), nullable and more specific constraints for certain types (e.g. maxLength, minLength, pattern for string values). While this is ok for basic checks, it’s not enough for complex or cross value checks.

To do something like this, the generator supports the x-constraints property which can be added to any schema definition and can be used in combination with the validation rules defined by OpenAPI. The value of this property can be a string or a list of strings. For each string you have to provide a function to perform your validation logic with this signature:

fun DefaultValidator.<function-name>(value: <value-type>) {
    // call fail(...) to raise a validation error    
}

Let’s create a little example to see this in action

type: string
minLength: 5
x-constraints:
  - withO
  - allLower

For this schema you have to provide two validation functions. The functions must be in the same package as the generated server or client code or in one of the packages specified via additionalImports in the plugin configuration.

fun DefaultValidator.withO(value: String) {
    if (!value.contains('o', ignoreCase = true)) {
        fail("must contain the letter 'o'", ErrorKind.Invalid)
    }
}

fun DefaultValidator.allLower(value: String) {
    if (value.lowercase() != value) {
        fail("must only be lowercase", ErrorKind.Invalid)
    }
}

The generated code would check that if a value was provided (because this schema defines a nullable type)

  • the length is at least 5
  • the function withO doesn’t fail
  • the function allLower doesn’t fail

In the example above, both checks could be realized by just using the pattern feature of OpenAPI. But imagine you have to check that a given date is at least 5 days in the future. This is not possible with OpenAPI but could be realized with this feature.

Furthermore, in both function the type of the value was a simple string. But that’s just because the schema defines a string type. By adding the x-constraints property to a schema of type object, array or any other type, the type of the value parameter changes accordingly.

Changing the names of enum items

The generator always tries to produce nice names for classes and properties. But sometimes the result is just not good enough. For example the following enum schemas

    PlainEnum:
      type: number
      format: int32
      enum:
        - 1
        - 2
        - 4
        - 8

    StringEnum:
      type: string
      enum:
        - x-flag-upper
        - x-flag-lower

will be converted into these kotlin enums

enum class PlainEnum(val value: Int) {
    _1(1),
    _2(2),
    _4(4),
    _8(8);
}

enum class StringEnum(val value: String) {
    XFlagUpper("x-flag-upper"),
    XFlagLower("x-flag-lower");
}

Unfortunately the generator has no additional information to generate better labels and changing the OpenAPI specification is not always possible, especially if it is provided by another party. For this, the generator supports the x-enum-item-names property which can be added to enum schema definitions. The value of this property is a map with string keys and string values. The generator will use this map when generating labels for enum items.

    PlainEnum:
      type: number
      format: int32
      enum:
        - 1
        - 2
        - 4
        - 8
      x-enum-item-names:
        1: One
        4: Four
        
    StringEnum:
      type: string
      enum:
        - x-flag-upper
        - x-flag-lower
      x-enum-item-names:
        x-flag-upper: Upper
        x-flag-lower: Lower

will now be converted into these kotlin enums

enum class PlainEnum(val value: Int) {
    One(1),
    _2(2),
    Four(4),
    _8(8);
}

enum class StringEnum(val value: String) {
    Upper("x-flag-upper"),
    Lower("x-flag-lower");
}

As you can see, this only changes the label not the value of an enum item. And you don’t have to specify a value for all items if you just want to modify some of them.

Changing the names of model classes

Similar to the names of enum items, the generator tries to generate meaningful names for model classes. If a schema is defined in the schemas section of the OpenAPI specification and referenced via $ref this name will be preferred if a model must be generated for this schema. In other cases, the generator uses the context where a schema is defined to find a name.

If you are not happy with the generated name of a model you can force the generator to use a different one. Just add the x-model-name property to a schema. The value of the property is a string, and it will be used as the name for this model if possible (the generator can still modify the name if it clashes with other generated files).

In the same way, the name for the wrapper class of a oneOf option can be changed with the x-container-model-name property. The value of this property is a string as well.

The following example uses both options

    Vehicle:
      oneOf:
        - type: object
          # this is a ship
          properties:
            numberOfDecks:
              type: integer
        - type: object
          # this is a car
          properties:
            numberOfWheels:
              type: integer
          x-model-name: Car
          x-container-model-name: CarOption

This oneOf schema will produce the following kotlin elements

sealed interface Vehicle {
    ...
}

data class VehicleVehicleOption1(val value: VehicleOption1) : Vehicle {
    ...
}

data class CarOption(val value: Car) : Vehicle {
    ...
}

As you can see, the model and the container for the first option was generated, while the model (Car) and container (CarOption) for the second option is as specified by the two properties.

Sharing responses between requests

It’s possible in OpenAPI to define common responses and reuse them in multiple requests.

In the following example, two requests use the same response to indicate a 400 error.

paths:
  /test1:
    get:
      operationId: test1
      responses:
        '400':
          $ref: '#/components/responses/Generic400'
        
  /test2:
    get:
      operationId: test2
      responses:
        '400':
          $ref: '#/components/responses/Generic400'
        
components:
  responses:
    Generic400:
      description: Bad Request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorInfo'

The generated request context will look like this

class Test1RequestContext(...) {
    ...
    fun badRequest(body: ErrorInfo): Nothing = status(400, "application/json", body.asJson().asString(dependencyContainer.objectMapper))
    ...
}

class Test2RequestContext(...) {
    ...
    fun badRequest(body: ErrorInfo): Nothing = status(400, "application/json", body.asJson().asString(dependencyContainer.objectMapper))
    ...
}

In both classes a similar method was generated to send a 400 error to the caller. But the compiler doesn’t know that they are the same. This can be changed with the x-generic-response-name property. The value of this property is a string and specifies the name of an interface that should be generated for this response.

So changing the above example to

components:
  responses:
    Generic400:
      description: Bad Request
      x-generic-response-name: GenericBadRequestResponse
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorInfo'

will change the generated code to

interface GenericBadRequestResponse {

    fun badRequest(body: ErrorInfo): Nothing
}

class Test1RequestContext(...): GenericBadRequestResponse {
    ...
    override fun badRequest(body: ErrorInfo): Nothing = status(400, "application/json", body.asJson().asString(dependencyContainer.objectMapper))
    ...
}

class Test2RequestContext(...): GenericBadRequestResponse {
    ...
    override fun badRequest(body: ErrorInfo): Nothing = status(400, "application/json", body.asJson().asString(dependencyContainer.objectMapper))
    ...
}

A new interface with this method was generated and is implemented by both context classes. This can be useful to implement crosscutting aspects. For example, the following method is now available for all requests with this response

fun <C> C.doSomething() where C: GenericBadRequestResponse {
    // can now generate a bad request response 
    badRequest(ErrorInfo(...))
}

override suspend fun Test1RequestContext.test1(): Nothing {
    ...
    doSomething()
    ...
}

Adding multiple interfaces to the where clause is also possible and a request needs all the specified responses in order to use this method.

fun <C> C.doSomethingElse() where C: GenericForbiddenResponse, C: GenericUnauthorizedResponse { }

The x-generic-response-name property can be used at any response not only on those under /components/responses as in the example above. But you have to make sure, all responses with the same property value are identically. This means same body model and same headers. Otherwise, you will run into compiler errors.