Including external OpenAPI models in your own OpenAPI definition

Filippos Karailanidis
XM Global
Published in
8 min readJun 16, 2022

We tried consuming an OpenAPI definition in a PHP Symfony application, and then exposing its models through our own OpenAPI definition, to be used by our frontend.

There is not a lot of documentation covering this use-case, so here’s a guide on how we made it work.

Introduction

Let’s take a look at a simplified version of the problem:

API → Frontend

Suppose we have 2 systems:

  • Core Backend Service
  • User-facing Frontend

The Core Backend Service exposes an OpenAPI definition. We can use it to generate a Typescript client for the Frontend. Now we can use the client (and easily re-generate it in case there are any changes).

API → BFF → Frontend

Sometimes, a middle system is needed in order to be a dedicated Backend for Frontend (BFF) for our application, for various reasons:

  • Access Control
  • Can talk with more APIs than just the Core API, while the Frontend still only sees one system.
  • Can customize requests
  • Can customize responses

The figure shows just the Core API, but there could be multiple APIs behind the BFF System

In those cases, we lose the direct communication between systems. The frontend can still use the models of the generated client, but not the API calls.

What if we wanted to generate an OpenAPI definition from BFF, and “forward” the Core definitions to the Frontend?

This is the practical example we will focus on below.

Our Solution

  1. Generate PHP client from Core
  2. Write PHP endpoint annotations in BFF (to expose Core’s Models)
  3. Generate TypeScript client from BFF

This approach gave us great flexibility because we can expose only the models that are related to the endpoints we call from BFF(we don’t consume the whole of Core System)

1 — Generate PHP Client from Core

First, download the OpenAPI cli generator here

Then use it to generate the PHP client code

java -jar openapi-generator-cli-5.3.0.jar generate \
-g php \
-i http://systemA.xm.com/openapi.json \
-o packages/xm/system-a-client/ \
--skip-validate-spec

The flag --skip-validate-spec might be needed in case the Core API definition is not complete. In our case, where we are slowly integrating swagger to our workflow, not all endpoints are fully defined. That’s why we use this flag to skip the validation step.

This will generate local PHP code that includes API calls and Models from the Core System. Then we make sure to install it so composer knows where to find it

//composer.json
{
...
"repositories": [
{
"type": "path",
"url": "packages/xm/*"
}
]
}

And run:

composer require xm/system-a-client

2 — Generating an OpenAPI definition in BFF

In order to generate an OpenAPI definition of our PHP system, we have two options for external libraries:

  • https://github.com/nelmio/NelmioApiDocBundle
    Since we are using Symfony, this is the easiest approach for us. This bundle has the “@Model” annotation that automatically generates OpenAPI schemas from our PHP classes. It is the approach we suggest, and the rest of the guide will use this library.
  • https://github.com/zircote/swagger-php
    This is the “low-level” package used by nelmio, and it provides all the annotations needed to decorate your PHP classes and manually set up everything that will go into the definition. If your needs are minimal and you have small objects, then you can keep your dependencies low and only use this package instead.

PHP Endpoint Annotations

After nelmio is configured, it automatically lists all of the applications endpoints in the default swagger definition.

We need to add annotations manually to each endpoint, to specify what is returned as a Response Model.

Below are some code examples:

Response of a Model object

    /**
* @OA\Response(
* response=200,
* description="Required Description",
* @OA\JsonContent(
* ref=@Model(type=MyModel::class)
* )
* )
*/

Response of a custom object

    /**
* @OA\Response(
* response=200,
* description="Required Description",
* @OA\JsonContent(
* type="object",
* @OA\Property(
* property="data",
* ref=@Model(type=MyModel::class)
* )
* )
* )
*/

Response of an array of objects

    /**
* @OA\Response(
* response=200,
* description="Required Description",
* @OA\JsonContent(
* type="array",
* @OA\Items(ref=@Model(type=MyModel::class))
* )
* )
*/

3 — Generate Typescript Client from BFF

Now, in order to be able to use these endpoints from the frontend, we need to run openapi-generator-cli again, only this time we will use BFF’s definition and generate a TypeScript client.

NOTE: By default nelmio is not installed as a dev dependency, which means the swagger ui will be visible in your production environment as well. In this case you can use the public URL to get the definition.

java -jar openapi-generator-cli-5.3.0.jar generate \
-g typescript-axios \
-i http://systemB.xm.com/openapi.json \
-o assets/js/systemBClient/ \
--skip-validate-spec

Hopefully by this time you are done and you have a fully working client in your frontend, ready to consume your endpoints with accurate Typescript models in place.

… unless something goes wrong!

Troubleshooting

As we mentioned above, this is not a really common use-case, and as a result there were a few hiccups during this process. Hopefully as the library matures, those issues will be fixed. Here is a list of the ones we faced, and how we managed to resolve them.

Issue #1 — Serialization

The Model FetchAdvancedSearchColumnsResponse seems to be empty. The reason is that the generated PHP classes are not compatible with the SerializerExtractor and ReflectionExtractor used by Symfony Property Info.

Thankfully we can create our own custom Extractor. By implementing the proper interface it is automatically tagged and loaded by the property-info component.

<?phpdeclare(strict_types=1);namespace App\Swagger;use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;class SwaggerPropertyExtractor implements PropertyListExtractorInterface
{
public function getProperties(string $class, array $context = []): ?array
{
if (!method_exists($class, 'openAPITypes')) {
return null;
}
return array_keys($class::openAPITypes());
}
}

The models that are generated by the openapi cli (under the -g php templates) contain a static function (openAPITypes) that has just the information we need.

Issue #2 — Definition Examples

Another issue we face is that “example” values for the fields are not preserved. This would be useful in case we want to use a mock server like prism later (https://stoplight.io/open-source/prism)

Even if the original definition had examples, they are not available to be exported to System B’s definition.

The solution is to use OpenApi annotations in generated PHP classes.

To do that we need to customize the mustache templates with which the classes are being generated.

Get the default mustache templates

java -jar openapi-generator-cli-5.3.0.jar \
author \
template \
-g php

The templates are now in a new out/ directory that has been generated.

We are interested in model.mustache and model_generic.mustache

First we need to import the annotations namespace.

//model.mustache...use OpenApi\Annotations as OA;
...

Then we use the Property annotation on the getter’s docblock

//model_generic.mustache
...
/**
* Gets {{name}}
{{#isPrimitiveType}}
* @OA\Property(example="{{example}}")
{{/isPrimitiveType}}
*
...

In mustache, {{#value}}{{/value}} checks if the value is null before printing the contents.

WARNING: Trying to use {{#example}}{{/example}} does not work. The generator cli currently has a bug where the example value is converted to “null” as a string, and is therefore not considered null. Thankfully the isPrimitiveType boolean works great in its place. You can debug your mustache parameters to avoid similar issues by following the instructions here https://openapi-generator.tech/docs/templating/#models

Generate the client using your custom templates

java -jar openapi-generator-cli-5.3.0.jar generate \
-g php \
-i http://systemA.xm.com/openapi.json \
-o packages/xm/system-a-client/ \
-t out/
--skip-validate-spec

You can move the out/ folder to a more appropriate directory, and also delete the template files that don’t need to be customized.

We now have the examples from the original definition!

Issue #3 — Required Properties

By default, Open Api Spec considers response properties to be optional, meaning that they might not be present in the response at all.

This means that we need to explicitly list our required properties that will definitely be in the response.

Just edit the model.mustache template to add the Schema annotation on top of the class

/**
* {{classname}} Class Doc Comment
*
* ...
* @OA\Schema(
* schema="{{classname}}",
* required={ {{#vars}}{{#required}}"{{name}}",{{/required}}{{/vars}} },
* )
*/

Conclusion

This process is a bit involved, but there is a lot of value once this is in place.

In our project we have large and complex objects in responses coming from the Core API. Being able to use TypeScript in the frontend to know the exact structure of the object and catch small type errors is a godsent. Not to mention this whole process happens automatically once the Core API objects are available by that system, just by re-generating the clients.

In addition this gives our backend and frontend teams freedom because they are now decoupled. There is no need for the frontend to wait for the backend to finish. The backend can write the contract using Annotations, and then the frontend can generate a mock server using those Annotations and start working on their side, while the backend works on the actual implementation.

Be sure to check prism out. Apart from the “mock” function, where it creates a mock server by just the OpenAPI definition, it has an even more interesting functionality. If you use “proxy” mode, it will listen to your actual API and compare the response you return vs the response you “promised” you will return (aka the OpenAPI definition). This can help the backend developers be more proactive in catching errors and making sure the definition is consistent with the actual response.

References:

  1. Nelmio Bundle Documentation
    https://symfony.com/bundles/NelmioApiDocBundle/current/index.html
  2. OpenAPI Generator Cli
    https://openapi-generator.tech/docs/usage
  3. Zircote Reference (Available Annotations)
    http://zircote.github.io/swagger-php/reference/
  4. Prism Mock Server
    https://github.com/stoplightio/prism

Technologies & versions used:

PHP 8.0.14

Symfony 6.0.7

nelmio/api-doc-bundle:v4.8.2 (using zircote/swagger-php:v4.2.14)

openapi-generator-cli-5.3.0.jar

--

--