Implementing OpenAPI 3.0 Contracts via Ballerina Service Annotations
Avoid manual documentation drift by using Ballerina's built-in OpenAPI generator to emit machine-readable contracts directly from your service code during the build process.
21 Mar 2026, 20:30 UTC

Requirements
Maintaining a separate OpenAPI specification file often leads to "documentation drift," where the actual API implementation diverges from the published contract. To eliminate this, teams require a mechanism where the source of truth is the code itself, and the machine-readable contract is a deterministic artifact of the build process.
The Smallest Suitable Design
Ballerina (version 1.2.x LTS and later) provides a built-in generator that transforms service annotations into an OpenAPI 3.0 document. The minimal implementation requires defining the service structure using specific annotations that the compiler can resolve statically.
Implementation Components
- @http:Service: Defines the base path and service-level metadata.
- @http:ResourceConfig: Specifies the HTTP method, the relative path, and the expected request/response formats.
- Concrete Records: Using Ballerina records instead of generic types ensures the generator can produce a precise JSON Schema.
Concrete Example
Run the following code in a Ballerina project. Ensure you have the ballerina/http module imported.
import ballerina/http;
@http:Service { basePath: "/api" }
service HelloService {
@http:ResourceConfig {
method: "GET",
path: "/greet",
produces = ["application/json"]
}
resource function getGreet(string name) returns HelloResponse {
return { message: "Hello " + name };
}
}
record HelloResponse {
string message;
}
To generate the contract, execute the build command from the project root terminal:
bal build
Expected Result: The compiler generates a file at target/openapi.yaml. This file contains the /api/greet path, the GET operation, and the HelloResponse schema definition.
Trust and Data Boundaries
The generator enforces boundaries based on language visibility and type specificity to ensure internal implementation details do not leak into the public contract.
Visibility Boundaries
Only public service resources are included in the emitted OpenAPI document. Any internal helper functions or private resources are ignored by the generator, maintaining a strict boundary between the API surface and the business logic.
Type Mapping and Safety
| Ballerina Type | OpenAPI Representation | Contract Strength |
|---|---|---|
| Record | JSON Schema Object | Strong |
| Enum / Union | oneOf / anyOf | Strong |
| json / xml | Generic object | Weak |
Risk: Using the generic json type removes the generator's ability to define specific fields, resulting in a generic object in the YAML. This reduces client-side type safety and should be avoided for public-facing contracts.
Operational Checks
To ensure the generated contract is accurate and the service adheres to it, integrate the following checks into your CI/CD pipeline.
1. Artifact Verification
Verify that the build process actually produced the contract. This prevents deployments where the documentation is missing.
# Run in CI shell
if [ ! -f "target/openapi.yaml" ]; then
echo "Error: OpenAPI contract was not generated"
exit 1
fi
2. Contract Conformance Testing
Use the ballerina/openapi validator to check if the running service actually behaves as the generated YAML claims. This catches discrepancies caused by imperative logic (e.g., manually setting status codes in code that aren't in the annotations).
# Run with required permissions to execute tests
bal test --openapi target/openapi.yaml --mode contract
Failure Modes
- Imperative Overrides: If you use
http:Responseto manually set a status code (e.g., 404) inside a function body, the OpenAPI generator will not see this. The contract will only show the return type's success code unless@http:ResponseCodesis explicitly used. - Dynamic Payloads: As noted in the data boundaries, using
jsontypes leads to an empty schema, which may cause client generators to fail or produce untyped code. - Version Mismatch: Versions prior to 1.2.x may require the
--experimentalflag and may not support the full OpenAPI 3.0 specification.
Conditions for Design Change
The annotation-driven approach is ideal for most services, but you should reconsider this design if:
- Complex Conditional Logic: Your API returns entirely different schemas based on complex runtime state that cannot be mapped to a Union type.
- External Governance: Your organization requires a "Design-First" workflow where the OpenAPI file is authored in a separate tool and used to generate the code, rather than the other way around.
- Multi-Protocol Requirements: You need a single contract covering both HTTP and non-HTTP protocols (like gRPC or WebSockets) that the standard HTTP generator does not support.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.