Summary
Add a declarative mechanism to annotate RESTHeart Service plugins with OpenAPI 3 metadata, and expose an auto-generated aggregated specification at a dedicated endpoint.
Currently RESTHeart has no built-in support for OpenAPI documentation. Plugin developers must maintain external spec files that can easily go out of sync with the actual implementation. The proposed mechanism is non-invasive: plugins without annotations continue to work (a fallback spec is auto-generated from @RegisterPlugin metadata), while plugins that opt in get precise, co-located documentation.
Motivation
- API consumers (frontend teams, third-party integrators) need a machine-readable contract to generate clients and write integration tests.
- The existing
@RegisterPlugin annotation already captures name, description, defaultURI, uriMatchPolicy, and secure — a natural foundation to extend.
- OpenAPI 3 is the de-facto standard for REST API documentation; tools like Swagger UI, Redoc, and
openapi-generator all consume it natively.
- A RESTHeart-native solution avoids the sprawl of hand-maintained YAML files that quickly diverge from actual plugin behaviour.
Proposed design
New annotations (in commons)
// Primary annotation — placed on the Service class alongside @RegisterPlugin
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface OpenApiOperation {
String summary() default "";
String description() default "";
String[] tags() default {};
String operationId() default ""; // defaults to @RegisterPlugin.name()
String[] methods() default {"GET", "POST", "PUT", "PATCH", "DELETE"};
boolean deprecated() default false;
OpenApiParam[] parameters() default {};
OpenApiRequestBody requestBody() default @OpenApiRequestBody(required = false);
OpenApiResponse[] responses() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@Target({}) // nested only
public @interface OpenApiParam {
String name();
String in(); // "query" | "path" | "header" | "cookie"
String description() default "";
boolean required() default false;
String schema() default "string"; // OpenAPI primitive type
String example() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface OpenApiRequestBody {
boolean required() default true;
String mediaType() default "application/json";
String schema() default ""; // JSON Schema inline or "$ref"
String description() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface OpenApiResponse {
int statusCode();
String description() default "";
String mediaType() default "application/json";
String schema() default ""; // JSON Schema inline or "$ref"
String example() default "";
}
Example usage on a plugin
@OpenApiOperation(
summary = "Ping the server",
description = "Returns a pong message, optionally echoing a custom query parameter.",
tags = {"health"},
methods = {"GET"},
parameters = {
@OpenApiParam(
name = "msg",
in = "query",
description = "Message to echo back",
required = false,
schema = "string",
example = "hello"
)
},
responses = {
@OpenApiResponse(statusCode = 200, description = "Pong", mediaType = "text/plain"),
@OpenApiResponse(statusCode = 401, description = "Unauthorized")
}
)
@RegisterPlugin(
name = "ping",
description = "Ping service",
defaultURI = "/ping",
secure = false,
blocking = false
)
public class PingService implements StringService { ... }
OpenApiService — aggregator plugin (in core)
A new built-in Service registered at /api/openapi.json (configurable):
- On
@OnInit, iterates PluginsRegistry#getServices().
- For each registered
Service, checks for @OpenApiOperation:
- Present → builds a
PathItem from the annotation.
- Absent → generates a minimal fallback
PathItem from @RegisterPlugin metadata and the inferred request/response type (see schema inference table below).
- Assembles an
OpenAPI object and caches its JSON representation.
- Serves it on
GET /api/openapi.json; returns 405 on other methods.
- Cache is invalidated on plugin hot-reload events.
An optional GET /api/openapi.yaml variant can be added in a follow-up.
SchemaInferrer — automatic schema from service type
| Service type |
Inferred request/response schema |
JsonService |
type: object (generic) |
BsonService |
type: object (annotated as MongoDB doc) |
StringService |
type: string |
ByteArrayService |
type: string, format: binary |
SseService |
no body; text/event-stream |
OpenApiBuilder — assembly utility
public class OpenApiBuilder {
public static JsonNode build(PluginsRegistry registry) {
OpenAPI spec = new OpenAPI()
.info(new Info().title("RESTHeart API").version(/* from manifest */));
for (PluginRecord<Service<?,?>> record : registry.getServices()) {
RegisterPlugin rp = record.getInstance().getClass().getAnnotation(RegisterPlugin.class);
OpenApiOperation oa = record.getInstance().getClass().getAnnotation(OpenApiOperation.class);
PathItem path = (oa != null)
? buildFromAnnotation(oa, rp)
: buildFallback(rp, record.getInstance());
spec.path(rp.defaultURI(), path);
}
return new ObjectMapper().valueToTree(spec);
}
}
New Maven dependency (in core/pom.xml)
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-core</artifactId>
<version>2.2.x</version>
</dependency>
Configuration
plugins-args:
openapi:
enabled: true
path: /api/openapi.json # default
include-auto-generated: true # include plugins without @OpenApiOperation
info:
title: "RESTHeart API"
version: "1.0"
contact-email: ""
Scope
- New annotations
@OpenApiOperation, @OpenApiParam, @OpenApiRequestBody, @OpenApiResponse added to commons.
OpenApiService, OpenApiBuilder, SchemaInferrer added to core.
- Built-in plugins (
ping, metrics, etc.) annotated as reference implementations.
- Unit tests for
OpenApiBuilder with annotated and non-annotated service stubs.
- Integration test:
GET /api/openapi.json → valid OpenAPI 3 document (validated against the OpenAPI 3.0 JSON Schema).
Tradeoffs considered
| Approach |
Pro |
Con |
| Annotations (proposed) |
Declarative, co-located with code, zero boilerplate for simple cases |
Verbose for complex schemas; no IDE-level schema validation |
OpenApiProvider interface |
Fully programmatic, any schema complexity |
Boilerplate; documentation scattered from registration metadata |
| External YAML/JSON files |
No compile-time dependency |
Easily goes out of sync; no discovery mechanism |
| Reflection on generics |
Zero annotations |
Cannot capture descriptions, examples, or response codes |
The annotation approach is consistent with the existing @RegisterPlugin pattern already familiar to plugin developers.
Open questions
- Should
@OpenApiOperation support per-method granularity (separate @Get, @Post, … annotations) or is a single methods[] array sufficient for the initial implementation?
- Should reusable schemas be definable globally (in
components/schemas) or only inline per-operation?
- Should the spec include MongoDB Collection API endpoints (proxied by the
mongodb module) or only custom Service plugins?
Summary
Add a declarative mechanism to annotate RESTHeart
Serviceplugins with OpenAPI 3 metadata, and expose an auto-generated aggregated specification at a dedicated endpoint.Currently RESTHeart has no built-in support for OpenAPI documentation. Plugin developers must maintain external spec files that can easily go out of sync with the actual implementation. The proposed mechanism is non-invasive: plugins without annotations continue to work (a fallback spec is auto-generated from
@RegisterPluginmetadata), while plugins that opt in get precise, co-located documentation.Motivation
@RegisterPluginannotation already capturesname,description,defaultURI,uriMatchPolicy, andsecure— a natural foundation to extend.openapi-generatorall consume it natively.Proposed design
New annotations (in
commons)Example usage on a plugin
OpenApiService— aggregator plugin (incore)A new built-in
Serviceregistered at/api/openapi.json(configurable):@OnInit, iteratesPluginsRegistry#getServices().Service, checks for@OpenApiOperation:PathItemfrom the annotation.PathItemfrom@RegisterPluginmetadata and the inferred request/response type (see schema inference table below).OpenAPIobject and caches its JSON representation.GET /api/openapi.json; returns405on other methods.An optional
GET /api/openapi.yamlvariant can be added in a follow-up.SchemaInferrer— automatic schema from service typeJsonServicetype: object(generic)BsonServicetype: object(annotated as MongoDB doc)StringServicetype: stringByteArrayServicetype: string,format: binarySseServicetext/event-streamOpenApiBuilder— assembly utilityNew Maven dependency (in
core/pom.xml)Configuration
Scope
@OpenApiOperation,@OpenApiParam,@OpenApiRequestBody,@OpenApiResponseadded tocommons.OpenApiService,OpenApiBuilder,SchemaInferreradded tocore.ping,metrics, etc.) annotated as reference implementations.OpenApiBuilderwith annotated and non-annotated service stubs.GET /api/openapi.json→ valid OpenAPI 3 document (validated against the OpenAPI 3.0 JSON Schema).Tradeoffs considered
OpenApiProviderinterfaceThe annotation approach is consistent with the existing
@RegisterPluginpattern already familiar to plugin developers.Open questions
@OpenApiOperationsupport per-method granularity (separate@Get,@Post, … annotations) or is a singlemethods[]array sufficient for the initial implementation?components/schemas) or only inline per-operation?mongodbmodule) or only customServiceplugins?