Lily is a compiler that consumes OpenAPI (Swagger) specifications and produces Java source code. It is intended to be an alternative to OpenAPI Generator, which at time of writing is the only option, and substantially more complete than Lily.
Please be aware that Lily is in the early stages of development and may not be suitable for production use-cases. Use Lily at your own risk.
That said, beta testing is the best way to contribute to this project.
Generated source code looks like this:
List<Pet> exampleHappyPath() {
var api = Api.newBuilder()
.uri("https://example.com/")
.build();
try {
var response = api.petsOperations()
.listPets()
.query(query -> query.limit(50))
.sendSync();
return switch (response) {
case ListPets200 ok -> ok.body().value();
case ListPetsDefault other -> throw new RuntimeException(other.body().message());
};
} catch (IOException | InterruptedException e) {
// The java.net.http layer encountered an exception.
throw new RuntimeException("Unable to complete Pets API request", e);
}
}
Here are some of the Lily features we just saw:
-
If an operation has the 'pets' tag, then we can access it via the
petsOperations()
operation group. Every operation is also part of theeveryOperation()
group, and operations without tags are also members of theeveryUntaggedOperation()
group. These groups are intended to help us explore the API using IDE type-ahead/auto-complete hints. -
Responses form a sealed interface. If we have the pattern-matching for switch expressions feature enabled, we can create an exhaustive switch expression to handle all possible responses, including undocumented and unexpected ones. Otherwise, we can use pattern-matching in an if-else ladder, or even access the status code via
response.httpResponse().statusCode()
(the native java.net.http API).
In the real world, OpenAPI specifications have errors in them that could prevent a generated API from successfully making requests. Rather than wait for service owners to update their specifications or try to fix them in a local copy ourselves, we can use Lily’s API to do as much as possible, then dip down into the underlying java.net.http API for full customization and control:
var operation = api.petsOperations()
.listPets()
.query(query -> query.limit(50));
var request = HttpRequest.newBuilder(operation.httpRequest(), (k, v) -> true)
.header("x-some-undocumented-header", "foo;bar;baz")
.build();
// If the API has correctly documented responses, lily will help us deserialize
// the response and we can handle it like before.
var response = operation.sendSync(request);
// Otherwise, we can use the httpClient to get an HttpRequest of an InputStream
// and deserialize it however we see fit, including not at all.
var response = api.httpClient().send(request, BodyHandlers.ofInputStream());
Here’s what we just saw:
-
We can use the operation to customize an HttpRequest, then use the java.net.http API to copy-and-modify that request. We can use Lily for everything that is documented by the OpenAPI specification correctly, but then arbitrarily modify the request with the native API. This lets us accommodate nearly any specification error, and even flaws in Lily.
-
We can then ask the operation to send the customized request, which will return a response that lazily deserializes the response body to the documented type. If we know the documented type is wrong, we can instead send the request with the native API and deserialize the InputStream however necessary, or not at all.
In other words, Lily is designed to facilitate HTTP interactions whenever possible, but fall back gracefully to the native java.net.http API in the presence of specification errors. Notably, all of these workarounds are forwards-compatible: Once the service owners update their OpenAPI specification to correct whatever errors were present, all of our code continues working. We can go back and update the code to use the generated API at our own pace.
To generate sources from an OAS document in your maven project, and the following maven build plugin and dependencies:
<build>
<plugins>
<plugin>
<groupId>io.github.tomboyo.lily</groupId>
<artifactId>lily-compiler-maven-plugin</artifactId>
<version>${lilyVersion}</version>
<configuration>
<!-- Any URI to an OAS document, be it https:// or file://. -->
<uri>https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml</uri>
<!-- Uncomment to customize the default generated sources directory. -->
<!-- <outputDir>target/generated-sources</outputDir> -->
<basePackage>com.exmaple.my.api</basePackage>
</configuration>
<executions>
<execution>
<goals>
<goal>compile-client</goal>
</goals>
<phase>generate-sources</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<!-- BEGIN generated code dependency management -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.13.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!-- END generated code dependency management -->
</dependencies>
</dependencyManagement>
<dependencies>
<!-- BEGIN Generated code dependencies -->
<dependency>
<groupId>io.github.tomboyo.lily</groupId>
<artifactId>lily-http</artifactId>
<version>${lilyVersion}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<!-- ZonedDatetime support -->
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- END Generated code dependencies -->
</dependencies>
The generated source code relies on jackson and the lily-http library at runtime, which is why these dependencies are necessary.
These configurations can be stand-alone or embedded in a larger project.
-
Generate java source code directly from an OAS document within a java build pipeline (e.g. integrated with Maven or Gradle).
-
Support OAS v3.
-
Target Java 17+, with special attention paid to upcoming language features.
-
Help end-users work around incorrect or incomplete schema specifications so that they can make progress while awaiting upstream fixes.
-
Expose a high-level API to guide the user through API interactions.
-
Ensure that whenever possible, generated source code is compatible with user code between API specification revisions. In other words: "If I update to the latest API specification, and there are not breaking changes to the API, then Lily’s generated source code doesn’t break my application."
-
Support all OpenAPI features, including unusual things like matrix-style requests.
-
Do not (yet) support other languages than Java. It’s not clear that a Java-oriented AST will cleanly translate to another language target.
-
Do not support too many options. Options become confusing to maintain — prefer opinionated code that works for most people who are doing sensible things.
Lily is a layered API with "high-level" layers that orchestrate full requests using generated code and "low-level" layers that help the developer implement requests from scratch if necessary.
High-level layers always allow the developer to move into lower levels. This allows the developer to use the convenient high-level API as much as possible, then resort to the lower-level API (which could be the java.net.http API itself) only as necessary to work around missing features or undocumented API parameters.
Lily should make simple things easy, and complex things possible.
Lily is composed of four modules in the modules
directory:
-
example
compiles the v3.0 petstore YAML as an example. Check out the generated-sources directory after a build to see what Lily generates, and the test directory to see example usage of the generated code. -
lily-compiler-maven-plugin
is a teensy-weensy Maven plugin that reads configuration from the pom and hands it off to the compiler project. This is what the user adds to their projects to compile code. -
lily-compiler
is responsible for reading an OAS document, translating it to an intermediary AST (abstract syntax tree), rendering the AST as source code, and finally saving source code to disc. -
lily-http
defines classes to help create and receive HTTP requests, including RFC6570 encoders, deser implementations, and the UriTemplate. This is a dependency of generated source code and may also be used directly by users to work around Lily or OAS shortcomings.