diff --git a/build.gradle.kts b/build.gradle.kts
index 5b1181ad..bc1fb52c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -17,7 +17,7 @@ allprojects {
apply(plugin = "maven-publish")
group = "io.javalin.community.openapi"
- version = "7.2.2"
+ version = "7.3.0-RC.1"
repositories {
mavenCentral()
diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs
index 2ee07078..f1685e20 100644
--- a/docs/.vitepress/config.mjs
+++ b/docs/.vitepress/config.mjs
@@ -56,7 +56,8 @@ export default defineConfig({
text: 'Advanced',
collapsed: false,
items: [
- { text: 'Compile-time Configuration', link: '/advanced/configuration' },
+ { text: 'Static Configuration', link: '/advanced/configuration' },
+ { text: 'Scripting Configuration', link: '/advanced/scripting' },
{ text: 'Runtime Builder DSL', link: '/advanced/runtime-builder' },
],
},
diff --git a/docs/advanced/configuration.md b/docs/advanced/configuration.md
index 4895cf52..eb11ba44 100644
--- a/docs/advanced/configuration.md
+++ b/docs/advanced/configuration.md
@@ -1,75 +1,14 @@
-# Compile-time Configuration
+# Static Configuration
-Configure the annotation processor using a Groovy script. This allows custom type mappings, property filters, and advanced type processors.
+Pass key-value options to the backend at build time. The `info.*` options apply to both the annotation processor (APT/Kapt) and KSP; `openapi.groovy.path` is APT/Kapt-only.
-## Setup
+## Options
-Create a Groovy configuration script (e.g. `openapi.groovy`) anywhere in your project
-and point the annotation processor to it using the `openapi.groovy.path` option:
-
-```groovy
-import io.javalin.openapi.experimental.*
-
-@ExperimentalCompileOpenApiConfiguration
-class OpenApiConfiguration
- implements OpenApiAnnotationProcessorConfigurer {
-
- @Override
- void configure(
- OpenApiAnnotationProcessorConfiguration configuration
- ) {
- // Configuration goes here
- }
-}
-```
-
-::: code-group
-
-```kotlin [Gradle (Kotlin)]
-kapt {
- arguments {
- arg(
- "openapi.groovy.path",
- "$projectDir/src/main/compile/openapi.groovy"
- )
- }
-}
-```
-
-```groovy [Gradle (Groovy)]
-kapt {
- arguments {
- arg(
- 'openapi.groovy.path',
- "$projectDir/src/main/compile/openapi.groovy"
- )
- }
-}
-```
-
-```xml [Maven]
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
- -Aopenapi.groovy.path=${project.basedir}/src/main/compile/openapi.groovy
-
-
-
-```
-
-:::
-
-## Annotation Processor Options
-
-The following options can be passed to the annotation processor:
-
-| Option | Description |
-|-------------------------|----------------------------------------------------------|
-| `openapi.info.title` | Set the `info.title` field in the generated specification |
-| `openapi.info.version` | Set the `info.version` field in the generated specification |
-| `openapi.groovy.path` | Path to the Groovy configuration script |
+| Option | Description |
+|------------------------|------------------------------------------------------------------------------------------------------|
+| `openapi.info.title` | Set the `info.title` field in the generated specification |
+| `openapi.info.version` | Set the `info.version` field in the generated specification |
+| `openapi.groovy.path` | Path to a Groovy script for advanced configuration (APT/Kapt only, see [Scripting Configuration](./scripting)) |
::: code-group
@@ -82,6 +21,13 @@ kapt {
}
```
+```kotlin [Gradle (KSP)]
+ksp {
+ arg("openapi.info.title", "My API")
+ arg("openapi.info.version", "1.0.0")
+}
+```
+
```xml [Maven]
org.apache.maven.plugins
@@ -97,108 +43,4 @@ kapt {
:::
-## Custom Type Mappings
-
-Map custom types to simple OpenAPI types:
-
-```groovy
-void configure(
- OpenApiAnnotationProcessorConfiguration configuration
-) {
- configuration.simpleTypeMappings[
- 'org.bson.types.ObjectId'
- ] = new SimpleType("string")
-
- configuration.simpleTypeMappings[
- 'com.example.CustomId'
- ] = new SimpleType(/* type */ "integer", /* format */ "int64")
-}
-```
-
-## Property Filters
-
-Control which properties are included in schemas:
-
-```groovy
-configuration.propertyInSchemeFilter = {
- ctx, type, property ->
- !property.simpleName
- .toString()
- .startsWith("internal")
-}
-```
-
-## Custom Type Processors
-
-Insert custom logic for handling specific types (e.g., unwrapping `AtomicReference`):
-
-```groovy
-configuration.insertEmbeddedTypeProcessor({
- EmbeddedTypeProcessorContext context ->
- if (context.type.simpleName == 'AtomicReference'
- && context.type.generics.size() == 1) {
- context.parentContext.typeSchemaGenerator.addType(
- context.scheme,
- context.type.generics[0],
- context.inlineRefs,
- context.references,
- false
- )
- return true // handled
- }
-
- return false // use default processing
-})
-```
-
-Custom type processors run before all built-in type processing, so they can override the default behavior for any type.
-
-## Debug Mode
-
-Enable debug output during annotation processing:
-
-```groovy
-configuration.debug = true
-```
-
-## Parser Validation
-
-Validate the generated specification with Swagger Parser:
-
-```groovy
-configuration.validateWithParser = true // default
-```
-
-## Full Example
-
-```groovy
-import io.javalin.openapi.experimental.*
-
-@ExperimentalCompileOpenApiConfiguration
-class OpenApiConfiguration
- implements OpenApiAnnotationProcessorConfigurer {
-
- @Override
- void configure(
- OpenApiAnnotationProcessorConfiguration configuration
- ) {
- configuration.simpleTypeMappings[
- 'org.bson.types.ObjectId'
- ] = new SimpleType("string")
-
- configuration.simpleTypeMappings[
- 'com.example.Money'
- ] = new SimpleType("string")
-
- configuration.propertyInSchemeFilter = {
- ctx, type, property ->
- !property.simpleName
- .toString()
- .startsWith("_")
- }
-
- configuration.debug = false
- configuration.validateWithParser = true
- }
-}
-```
+For custom type mappings, property filters, and custom type processors, see [Scripting Configuration](./scripting).
diff --git a/docs/advanced/runtime-builder.md b/docs/advanced/runtime-builder.md
index 361ee006..8acfdeb6 100644
--- a/docs/advanced/runtime-builder.md
+++ b/docs/advanced/runtime-builder.md
@@ -4,9 +4,9 @@ The `OpenApiSchemaBuilder` provides a Kotlin DSL for building and modifying Open
## When to Use
-- **Extend compile-time schemas** — add servers, security, or extra endpoints at startup
-- **Dynamic endpoints** — describe routes registered at runtime that the annotation processor can't see
-- **Testing** — build expected schemas in tests without JSON strings
+- **Extend compile-time schemas** - add servers, security, or extra endpoints at startup
+- **Dynamic endpoints** - describe routes registered at runtime that the annotation processor can't see
+- **Testing** - build expected schemas in tests without JSON strings
## Basic Usage
@@ -41,21 +41,21 @@ The `SchemaBuilder` DSL provides a clean way to define inline schemas without wo
```kotlin
schema { type("string") }
-// → { "type": "string" }
+// -> { "type": "string" }
```
### Type with Format
```kotlin
schema { type("integer"); format("int32") }
-// → { "type": "integer", "format": "int32" }
+// -> { "type": "integer", "format": "int32" }
```
### Reference
```kotlin
schema { ref("#/components/schemas/User") }
-// → { "$ref": "#/components/schemas/User" }
+// -> { "$ref": "#/components/schemas/User" }
```
The schema DSL is available on media types, parameters, headers, and object schema properties.
@@ -217,7 +217,67 @@ schema.path("/users").operation("get") {
val json = schema.toJson()
```
-Reopening an existing operation preserves all fields — only the fields you set are changed. This makes it safe to layer runtime additions on top of compile-time output.
+Reopening an existing operation preserves all fields - only the fields you set are changed. This makes it safe to layer runtime additions on top of compile-time output.
+
+## Auto-generating Docs for Registered Routes
+
+For a springdoc-style experience - documenting routes that are registered programmatically (and that the compile-time processor never sees) - use the **dynamic hook** module. It adds undocumented Javalin routes to the served document.
+
+```kotlin [Gradle (Kotlin)]
+dependencies {
+ val openapi = "7.3.0-RC.1"
+ implementation("io.javalin.community.openapi:javalin-openapi-dynamic-hook:$openapi")
+}
+```
+
+Register `RegisteredRoutesHook` on the `OpenApiPlugin`:
+
+```kotlin
+Javalin.start { config ->
+ config.registerPlugin(OpenApiPlugin { it.withHook(RegisteredRoutesHook()) })
+
+ config.routes.get("/users") { /* ... */ }
+ config.routes.get("/users/{id}") { /* ... */ }
+}
+```
+
+Every newly discovered route is documented with its path, method, path parameters (typed as `string`), and a default `200` response. Existing operations from compile-time documentation are left unchanged unless the route carries runtime metadata.
+
+Routes registered by `OpenApiPlugin`, `SwaggerPlugin`, and `ReDocPlugin` are excluded by default, including custom UI and WebJar paths. Add prefix exclusions for your own non-API routes:
+
+```kotlin
+RegisteredRoutesHook { routes ->
+ routes.withIgnoredPathPrefixes("/assets", "/internal")
+}
+```
+
+Each prefix matches the path itself and its descendants, so `/assets` excludes `/assets/logo.svg` but not `/assets-admin`. A trailing `/*` is accepted as an equivalent spelling. To include the default excluded plugin routes, call `clearDefaultIgnoredRoutes()`.
+
+### Enriching a Route
+
+Javalin handlers are opaque lambdas, so without extra information the hook can only emit those stubs - it cannot infer request/response bodies. Attach an `OpenApiMetadata` to a route to describe it using the same operation DSL shown above; `schema(Class)` resolves the type through the reflection schema engine (no annotation processing required):
+
+```kotlin
+config.routes.addEndpoint(
+ Endpoint.create(HandlerType.GET, "/users/{id}")
+ .addMetadata(OpenApiMetadata {
+ summary("Get a user")
+ responses {
+ response("200") {
+ description("The user")
+ content { mediaType("application/json") { schema(User::class.java) } }
+ }
+ }
+ })
+ .handler { /* ... */ }
+)
+```
+
+For newly discovered operations, the path-parameter skeleton is auto-added before your metadata is applied. Existing operations retain their parameters, and referenced types (e.g. `User`) are emitted into `components/schemas`.
+
+::: warning
+The document is built on its first request, so routes registered after that request are not included. Runtime reflection cannot see CLASS-retention annotations or automatically discover discriminator subtypes. Runtime reflection is opt-in: it only happens when you add this module and register the hook, so the compile-time backends stay reflection-free.
+:::
## Merge Behavior
diff --git a/docs/advanced/scripting.md b/docs/advanced/scripting.md
new file mode 100644
index 00000000..bb09cd83
--- /dev/null
+++ b/docs/advanced/scripting.md
@@ -0,0 +1,203 @@
+# Scripting Configuration
+
+For advanced configuration - custom type mappings, property filters, and custom type processors - the annotation processor can run a Groovy script at compile time.
+
+::: warning Experimental, and intentionally temporary
+Scripting (`@ExperimentalCompileOpenApiConfiguration`) is a stop-gap gateway that exists only because there are no built-in static options for these advanced cases yet. It is **experimental**: its API may change, and the goal is to fold its capabilities into proper [static configuration](./configuration) over time, so this scripting layer may shrink or fade away in future releases. Prefer static options where they exist; reach for scripting only when you must.
+
+It also applies to the **APT/Kapt backend only** - it is not loaded by the KSP backend.
+:::
+
+## Setup
+
+Create a Groovy configuration script (e.g. `openapi.groovy`) anywhere in your project
+and point the annotation processor at it with the `openapi.groovy.path` option:
+
+```groovy
+import io.javalin.openapi.experimental.*
+
+@ExperimentalCompileOpenApiConfiguration
+class OpenApiConfiguration
+ implements OpenApiAnnotationProcessorConfigurer {
+
+ @Override
+ void configure(
+ OpenApiAnnotationProcessorConfiguration configuration
+ ) {
+ // Configuration goes here
+ }
+}
+```
+
+::: code-group
+
+```kotlin [Gradle (Kotlin)]
+kapt {
+ arguments {
+ arg(
+ "openapi.groovy.path",
+ "$projectDir/src/main/compile/openapi.groovy"
+ )
+ }
+}
+```
+
+```groovy [Gradle (Groovy)]
+kapt {
+ arguments {
+ arg(
+ 'openapi.groovy.path',
+ "$projectDir/src/main/compile/openapi.groovy"
+ )
+ }
+}
+```
+
+```xml [Maven]
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ -Aopenapi.groovy.path=${project.basedir}/src/main/compile/openapi.groovy
+
+
+
+```
+
+:::
+
+## Custom Type Mappings
+
+Map custom types to simple OpenAPI types:
+
+```groovy
+void configure(
+ OpenApiAnnotationProcessorConfiguration configuration
+) {
+ configuration.simpleTypeMappings[
+ 'org.bson.types.ObjectId'
+ ] = new SimpleType("string")
+
+ configuration.simpleTypeMappings[
+ 'com.example.CustomId'
+ ] = new SimpleType(/* type */ "integer", /* format */ "int64")
+}
+```
+
+## Property Filters
+
+Control which properties are included in schemas:
+
+```groovy
+configuration.propertyInSchemeFilter = {
+ ctx, type, property ->
+ !property.simpleName
+ .toString()
+ .startsWith("internal")
+}
+```
+
+## Custom Type Processors
+
+Insert custom logic for handling specific types (e.g., unwrapping `AtomicReference`):
+
+```groovy
+configuration.insertEmbeddedTypeProcessor({
+ EmbeddedTypeProcessorContext context ->
+ if (context.type.simpleName == 'AtomicReference'
+ && context.type.generics.size() == 1) {
+ context.parentContext.typeSchemaGenerator.addType(
+ context.scheme,
+ context.type.generics[0],
+ context.inlineRefs,
+ context.references,
+ false
+ )
+ return true // handled
+ }
+
+ return false // use default processing
+})
+```
+
+Custom type processors run before all built-in type processing, so they can override the default behavior for any type.
+
+## Debug Mode
+
+Enable debug output during annotation processing:
+
+```groovy
+configuration.debug = true
+```
+
+## Parser Validation
+
+Validate the generated specification with Swagger Parser:
+
+```groovy
+configuration.validateWithParser = true // default
+```
+
+## Full Example
+
+```groovy
+import io.javalin.openapi.experimental.*
+
+@ExperimentalCompileOpenApiConfiguration
+class OpenApiConfiguration
+ implements OpenApiAnnotationProcessorConfigurer {
+
+ @Override
+ void configure(
+ OpenApiAnnotationProcessorConfiguration configuration
+ ) {
+ configuration.simpleTypeMappings[
+ 'org.bson.types.ObjectId'
+ ] = new SimpleType("string")
+
+ configuration.simpleTypeMappings[
+ 'com.example.Money'
+ ] = new SimpleType("string")
+
+ configuration.propertyInSchemeFilter = {
+ ctx, type, property ->
+ !property.simpleName
+ .toString()
+ .startsWith("_")
+ }
+
+ configuration.debug = false
+ configuration.validateWithParser = true
+ }
+}
+```
+
+## Migrating your `openapi.groovy`
+
+The scripting API renamed two types. If your script references the schema type or its mirror accessor - typically inside `propertyInSchemeFilter` - update these names:
+
+| Before | After |
+|---------------------------|-----------------------|
+| `ClassDefinition` | `OpenApiType` |
+| `ClassDefinitionHandleKt` | `OpenApiTypeHandleKt` |
+
+```groovy
+// before
+import io.javalin.openapi.experimental.ClassDefinition
+import io.javalin.openapi.experimental.ClassDefinitionHandleKt
+
+configuration.propertyInSchemeFilter = { ctx, ClassDefinition type, property ->
+ ctx.isAssignable(ClassDefinitionHandleKt.getMirror(type), ...)
+}
+
+// after
+import io.javalin.openapi.experimental.OpenApiType
+import io.javalin.openapi.experimental.OpenApiTypeHandleKt
+
+configuration.propertyInSchemeFilter = { ctx, OpenApiType type, property ->
+ ctx.isAssignable(OpenApiTypeHandleKt.getMirror(type), ...)
+}
+```
+
+Everything else is unchanged: `simpleTypeMappings`, `insertEmbeddedTypeProcessor`, `debug`, `validateWithParser`, and the `OpenApiAnnotationProcessorConfiguration`/`OpenApiAnnotationProcessorConfigurer`/`SimpleType`/`EmbeddedTypeProcessorContext` types all keep their names and members. Scripts that don't name `ClassDefinition`/`ClassDefinitionHandleKt` need no changes.
diff --git a/docs/index.md b/docs/index.md
index 1f05d80a..0a54d58e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -4,7 +4,7 @@ layout: home
hero:
name: Javalin OpenAPI
text: Compile-time API Documentation
- tagline: Generate OpenAPI specifications and JSON Schemas at compile time with zero runtime reflection. Annotation-driven, type-safe, and ready for Swagger UI and ReDoc.
+ tagline: Generate OpenAPI specifications and JSON Schemas at compile time - via Java annotation processing, kapt, or KSP. Annotation-driven, type-safe, and ready for Swagger UI and ReDoc.
actions:
- theme: brand
text: Get Started
@@ -15,9 +15,11 @@ hero:
features:
- title: Compile-time Generation
- details: Schemas are generated during compilation using annotation processing. No runtime reflection, no classpath scanning, no startup overhead.
+ details: Schemas are generated during compilation via Java annotation processing, kapt, or KSP. No runtime reflection, no classpath scanning, no startup overhead.
- title: Two Modes
- details: Generate OpenAPI 3.1.0 endpoint documentation with @OpenApi, or standalone JSON Schema 2020-12 files with @JsonSchema — using the same annotation processor.
+ details: Generate OpenAPI 3.1.0 endpoint documentation with @OpenApi, or standalone JSON Schema 2020-12 files with @JsonSchema, from the same engine.
+ - title: Optional Runtime Backend
+ details: An opt-in reflection backend documents routes registered programmatically at runtime, for the dynamic cases the compile-time processor can't see.
- title: Swagger UI & ReDoc
details: Built-in plugins serve Swagger UI and ReDoc out of the box. Register the plugin and your interactive API documentation is live.
- title: Schema Customization
@@ -25,5 +27,5 @@ features:
- title: Enum Support
details: Rename values with @OpenApiName, apply naming strategies with @OpenApiNaming, create integer enums with @OpenApiPropertyType, and add descriptions.
- title: Compile-time Configuration
- details: Fine-tune the annotation processor with openapi.groovy — custom type mappings, property filters, and embedded type processors.
+ details: Set API info through build options. For advanced cases like custom type mappings, an experimental Groovy hook is available while first-class options grow.
---
diff --git a/docs/introduction/json-schema-setup.md b/docs/introduction/json-schema-setup.md
index a1bf71a5..96dde8f1 100644
--- a/docs/introduction/json-schema-setup.md
+++ b/docs/introduction/json-schema-setup.md
@@ -10,42 +10,48 @@ You can serve them with any HTTP server, use them for validation, feed them to c
## Installation
-You only need the annotation processor and the specification module — no Javalin plugins required:
+You only need the annotation processor and the specification module - no Javalin plugins required:
::: code-group
```kotlin [Gradle (Kotlin)]
dependencies {
- val openapi = "7.2.2"
-
- annotationProcessor(
- "io.javalin.community.openapi:openapi-annotation-processor:$openapi"
- )
- implementation(
- "io.javalin.community.openapi:openapi-specification:$openapi"
- )
+ val openapi = "7.3.0-RC.1"
+
+ annotationProcessor("io.javalin.community.openapi:openapi-annotation-processor:$openapi")
+ implementation("io.javalin.community.openapi:openapi-specification:$openapi")
}
```
```kotlin [Gradle (Kotlin) with Kapt]
dependencies {
- val openapi = "7.2.2"
-
- kapt(
- "io.javalin.community.openapi:openapi-annotation-processor:$openapi"
- )
- implementation(
- "io.javalin.community.openapi:openapi-specification:$openapi"
- )
+ val openapi = "7.3.0-RC.1"
+
+ kapt("io.javalin.community.openapi:openapi-annotation-processor:$openapi")
+ implementation("io.javalin.community.openapi:openapi-specification:$openapi")
+}
+```
+
+```kotlin [Gradle (Kotlin) with KSP]
+plugins {
+ // KSP is released against a specific Kotlin version - use the build that matches yours (github.com/google/ksp/releases)
+ id("com.google.devtools.ksp") version "2.3.9"
+}
+
+dependencies {
+ val openapi = "7.3.0-RC.1"
+
+ ksp("io.javalin.community.openapi:openapi-ksp:$openapi")
+ implementation("io.javalin.community.openapi:openapi-specification:$openapi")
}
```
-```xml [Maven]
+```xml [Maven (Java)]
io.javalin.community.openapi
openapi-specification
- 7.2.2
+ 7.3.0-RC.1
@@ -59,7 +65,7 @@ dependencies {
io.javalin.community.openapi
openapi-annotation-processor
- 7.2.2
+ 7.3.0-RC.1
@@ -68,6 +74,62 @@ dependencies {
```
+```xml [Maven (Kotlin)]
+
+ 2.1.0
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ io.javalin.community.openapi
+ openapi-specification
+ 7.3.0-RC.1
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ kapt
+
+ kapt
+
+
+
+ src/main/kotlin
+
+
+
+ io.javalin.community.openapi
+ openapi-annotation-processor
+ 7.3.0-RC.1
+
+
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+
+
+
+```
+
:::
## Annotate Your Types
@@ -106,14 +168,14 @@ At compile time, this generates a `/json-schemes/com.example.UserConfig` resourc
### Differences between the Two Modes
-| Aspect | OpenAPI | JSON Schema |
-|--------|---------|-------------|
-| Output format | OpenAPI 3.1.0 | JSON Schema 2020-12 |
-| Scope | Endpoint docs | Standalone type schemas |
-| References | `$ref` | All types inlined |
-| Trigger | `@OpenApi` | `@JsonSchema` |
+| Aspect | OpenAPI | JSON Schema |
+|---------------|---------------|-------------------------|
+| Output format | OpenAPI 3.1.0 | JSON Schema 2020-12 |
+| Scope | Endpoint docs | Standalone type schemas |
+| References | `$ref` | All types inlined |
+| Trigger | `@OpenApi` | `@JsonSchema` |
-Both modes are framework-agnostic at the annotation processing level — Javalin is only needed if you want to serve the specs via the Javalin plugins.
+Both modes are framework-agnostic at the annotation processing level - Javalin is only needed if you want to serve the specs via the Javalin plugins.
## Loading OpenAPI Specs at Runtime
@@ -122,7 +184,7 @@ Use `OpenApiLoader` to load the generated OpenAPI specifications from your class
```kotlin
val loader = OpenApiLoader()
-// version → JSON
+// version -> JSON
val schemes = loader.loadOpenApiSchemes()
for ((version, json) in schemes) {
@@ -171,17 +233,17 @@ class InternalEntity
Most property-level annotations work in both OpenAPI and JSON Schema modes:
-- `@OpenApiName` — override property names
-- `@OpenApiDescription` — add descriptions
-- `@OpenApiIgnore` — exclude properties
-- `@OpenApiRequired` — force required
-- `@OpenApiPropertyType` — override types
-- `@OpenApiNullable` — mark nullable
-- `@OpenApiNaming` — apply naming strategies
-- `@OpenApiByFields` — resolve from fields instead of getters
-- Validation annotations — `@OpenApiNumberValidation`, `@OpenApiStringValidation`, `@OpenApiArrayValidation`, `@OpenApiObjectValidation`
+- `@OpenApiName` - override property names
+- `@OpenApiDescription` - add descriptions
+- `@OpenApiIgnore` - exclude properties
+- `@OpenApiRequired` - force required
+- `@OpenApiPropertyType` - override types
+- `@OpenApiNullable` - mark nullable
+- `@OpenApiNaming` - apply naming strategies
+- `@OpenApiByFields` - resolve from fields instead of getters
+- Validation annotations - `@OpenApiNumberValidation`, `@OpenApiStringValidation`, `@OpenApiArrayValidation`, `@OpenApiObjectValidation`
## Next Steps
-- [Type Composition](../json-schema/getting-started) — `@OneOf`, `@AnyOf`, `@AllOf` with discriminators
-- [Custom Properties](../json-schema/custom-properties) — add custom schema properties with `@Custom`
+- [Type Composition](../json-schema/getting-started) - `@OneOf`, `@AnyOf`, `@AllOf` with discriminators
+- [Custom Properties](../json-schema/custom-properties) - add custom schema properties with `@Custom`
diff --git a/docs/introduction/migration-from-6x.md b/docs/introduction/migration-from-6x.md
index 597f8aa7..9176df66 100644
--- a/docs/introduction/migration-from-6x.md
+++ b/docs/introduction/migration-from-6x.md
@@ -48,8 +48,8 @@ New `@OpenApiNaming` annotation applies automatic name transformation to propert
```kotlin
@OpenApiNaming(OpenApiNamingStrategy.SNAKE_CASE)
class UserResponse(
- val firstName: String, // → "first_name"
- val lastName: String, // → "last_name"
+ val firstName: String, // -> "first_name"
+ val lastName: String, // -> "last_name"
)
```
@@ -95,7 +95,7 @@ New built-in type mappings:
### Simplified Plugin API
-Security methods are available directly on `OpenApiSchemaBuilder` — no more `withSecurity { ... }` wrapper:
+Security methods are available directly on `OpenApiSchemaBuilder` - no more `withSecurity { ... }` wrapper:
```kotlin
openApiConfig.withDefinitionConfiguration { version, builder ->
@@ -130,11 +130,11 @@ Update the version in your build file:
::: code-group
```kotlin [Gradle (Kotlin)]
-val openapi = "7.2.2" // was 6.x
+val openapi = "7.3.0-RC.1" // was 6.x
```
```xml [Maven]
-7.2.2
+7.3.0-RC.1
```
:::
@@ -184,7 +184,7 @@ kapt {
io.javalin.community.openapi
openapi-annotation-processor
- 7.2.2
+ 7.3.0-RC.1
@@ -198,7 +198,7 @@ kapt {
If you don't use a Groovy configuration script, no changes are needed.
-### OpenAPI 3.0.3 → 3.1.0 {#openapi-303-310}
+### OpenAPI 3.0.3 -> 3.1.0 {#openapi-303-310}
Generated specs are now **OpenAPI 3.1.0** (was 3.0.3), based on JSON Schema 2020-12. This changes the generated output in several ways.
@@ -232,7 +232,7 @@ Nullable `$ref` types are wrapped in `anyOf`:
Nullable `oneOf`/`anyOf` types append a null entry:
```json
-// 7.0 — oneOf with nullable
+// 7.0 - oneOf with nullable
{
"oneOf": [
{ "$ref": "#/components/schemas/Cat" },
@@ -242,7 +242,7 @@ Nullable `oneOf`/`anyOf` types append a null entry:
}
```
-**No annotation changes needed** — this is handled automatically by the generator. If you have tooling that parses the generated spec (validators, code generators, etc.), it needs to understand 3.1.0 nullable semantics.
+**No annotation changes needed** - this is handled automatically by the generator. If you have tooling that parses the generated spec (validators, code generators, etc.), it needs to understand 3.1.0 nullable semantics.
#### `additionalProperties` default
@@ -253,10 +253,10 @@ Nullable `oneOf`/`anyOf` types append a null entry:
Changed from `Boolean` flags to numeric `String` values, matching JSON Schema 2020-12:
```kotlin
-// 7.0 — numeric value (the exclusive bound itself)
+// 7.0 - numeric value (the exclusive bound itself)
@OpenApiNumberValidation(exclusiveMinimum = "0")
-// 6.x — boolean flag (means: minimum is exclusive)
+// 6.x - boolean flag (means: minimum is exclusive)
@OpenApiNumberValidation(minimum = "0", exclusiveMinimum = true)
```
@@ -311,9 +311,9 @@ openApiConfig.withDefinitionConfiguration { version, definition ->
```
Key differences:
-- `withInfo(...)` → `info(...)`
-- `withServer(...)` → `server(...)`
-- Security methods (`withBearerAuth`, `withBasicAuth`, `withOAuth2`, etc.) moved from `SecurityComponentConfiguration` to `OpenApiSchemaBuilder` directly — no more `withSecurity(security -> ...)` wrapper
+- `withInfo(...)` -> `info(...)`
+- `withServer(...)` -> `server(...)`
+- Security methods (`withBearerAuth`, `withBasicAuth`, `withOAuth2`, etc.) moved from `SecurityComponentConfiguration` to `OpenApiSchemaBuilder` directly - no more `withSecurity(security -> ...)` wrapper
- `withDefinitionProcessor` moved from `DefinitionConfiguration` to `OpenApiPluginConfiguration`
#### Swagger & ReDoc configuration style
diff --git a/docs/introduction/redoc.md b/docs/introduction/redoc.md
index 7559abb5..315981bb 100644
--- a/docs/introduction/redoc.md
+++ b/docs/introduction/redoc.md
@@ -36,4 +36,4 @@ When running behind a reverse proxy that adds a base path, set `basePath` so the
redoc.basePath = "/api"
```
-`ReDocPlugin` is repeatable — you can register multiple instances for different configurations.
+`ReDocPlugin` is repeatable - you can register multiple instances for different configurations.
diff --git a/docs/introduction/setup.md b/docs/introduction/setup.md
index f59841db..8613ff87 100644
--- a/docs/introduction/setup.md
+++ b/docs/introduction/setup.md
@@ -19,22 +19,14 @@ repositories {
}
dependencies {
- val openapi = "7.2.2"
+ val openapi = "7.3.0-RC.1"
- annotationProcessor(
- "io.javalin.community.openapi:openapi-annotation-processor:$openapi"
- )
- implementation(
- "io.javalin.community.openapi:javalin-openapi-plugin:$openapi"
- )
+ annotationProcessor("io.javalin.community.openapi:openapi-annotation-processor:$openapi")
+ implementation("io.javalin.community.openapi:javalin-openapi-plugin:$openapi")
// Optional: Swagger UI
- implementation(
- "io.javalin.community.openapi:javalin-swagger-plugin:$openapi"
- )
+ implementation("io.javalin.community.openapi:javalin-swagger-plugin:$openapi")
// Optional: ReDoc
- implementation(
- "io.javalin.community.openapi:javalin-redoc-plugin:$openapi"
- )
+ implementation("io.javalin.community.openapi:javalin-redoc-plugin:$openapi")
}
```
@@ -44,41 +36,54 @@ plugins {
}
dependencies {
- val openapi = "7.2.2"
+ val openapi = "7.3.0-RC.1"
- kapt(
- "io.javalin.community.openapi:openapi-annotation-processor:$openapi"
- )
- implementation(
- "io.javalin.community.openapi:javalin-openapi-plugin:$openapi"
- )
- implementation(
- "io.javalin.community.openapi:javalin-swagger-plugin:$openapi"
- )
- implementation(
- "io.javalin.community.openapi:javalin-redoc-plugin:$openapi"
- )
+ kapt("io.javalin.community.openapi:openapi-annotation-processor:$openapi")
+ implementation("io.javalin.community.openapi:javalin-openapi-plugin:$openapi")
+ implementation("io.javalin.community.openapi:javalin-swagger-plugin:$openapi")
+ implementation("io.javalin.community.openapi:javalin-redoc-plugin:$openapi")
+}
+```
+
+```kotlin [Gradle (Kotlin) with KSP]
+plugins {
+ // KSP is released against a specific Kotlin version - use the build that matches yours (github.com/google/ksp/releases)
+ id("com.google.devtools.ksp") version "2.3.9"
+}
+
+dependencies {
+ val openapi = "7.3.0-RC.1"
+
+ ksp("io.javalin.community.openapi:openapi-ksp:$openapi")
+ implementation("io.javalin.community.openapi:javalin-openapi-plugin:$openapi")
+ implementation("io.javalin.community.openapi:javalin-swagger-plugin:$openapi")
+ implementation("io.javalin.community.openapi:javalin-redoc-plugin:$openapi")
+}
+
+ksp {
+ arg("openapi.info.title", "My API")
+ arg("openapi.info.version", "1.0.0")
}
```
-```xml [Maven]
+```xml [Maven (Java)]
io.javalin.community.openapi
javalin-openapi-plugin
- 7.2.2
+ 7.3.0-RC.1
io.javalin.community.openapi
javalin-swagger-plugin
- 7.2.2
+ 7.3.0-RC.1
io.javalin.community.openapi
javalin-redoc-plugin
- 7.2.2
+ 7.3.0-RC.1
@@ -92,7 +97,7 @@ dependencies {
io.javalin.community.openapi
openapi-annotation-processor
- 7.2.2
+ 7.3.0-RC.1
@@ -101,6 +106,90 @@ dependencies {
```
+```xml [Maven (Kotlin)]
+
+ 2.1.0
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+ ${kotlin.version}
+
+
+ io.javalin.community.openapi
+ javalin-openapi-plugin
+ 7.3.0-RC.1
+
+
+
+ io.javalin.community.openapi
+ javalin-swagger-plugin
+ 7.3.0-RC.1
+
+
+
+ io.javalin.community.openapi
+ javalin-redoc-plugin
+ 7.3.0-RC.1
+
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+ ${kotlin.version}
+
+
+ kapt
+
+ kapt
+
+
+
+ src/main/kotlin
+
+
+
+ io.javalin.community.openapi
+ openapi-annotation-processor
+ 7.3.0-RC.1
+
+
+
+
+
+ compile
+ compile
+
+ compile
+
+
+
+
+
+
+```
+
+:::
+
+## Choosing a Processor
+
+The same generation engine runs on three backends - pick the one that matches your build:
+
+| Backend | Dependency | Sources | Best for |
+|-----------------------------|--------------------------------|---------------|--------------------------------------------|
+| APT (`annotationProcessor`) | `openapi-annotation-processor` | Java | Java projects, or Kotlin via `kapt` |
+| Kapt (`kapt`) | `openapi-annotation-processor` | Java + Kotlin | Mixed Java/Kotlin projects |
+| KSP (`ksp`) | `openapi-ksp` *(experimental)* | Kotlin only | Kotlin-only projects wanting faster builds |
+
+All three emit the same `openapi-plugin/openapi-*.json` resource format that the `OpenApiPlugin` serves. APT/Kapt remain the reference backends; KSP is experimental and has the limitations listed below.
+
+::: warning KSP limitations
+KSP processes **Kotlin sources only** - `@OpenApi`/`@JsonSchema` on Java types are not picked up (use APT/Kapt for those). The Groovy [scripting configuration](../advanced/scripting) (custom type mappings, property filters, custom type processors) and parser validation are currently APT/Kapt-only. `@OpenApiByFields(only = true)` is also APT/Kapt-only; the KSP backend fails the build with a clear error instead of generating an empty schema.
:::
## Register the Plugin
@@ -206,11 +295,11 @@ openapi.withDefinitionConfiguration { version, builder ->
}
```
-`OpenApiPlugin` is repeatable — you can register multiple instances for different configurations.
+`OpenApiPlugin` is repeatable - you can register multiple instances for different configurations.
## Next Steps
-- [Javalin Swagger UI](./swagger) — interactive API explorer
-- [Javalin ReDoc](./redoc) — clean API reference
-- [OpenAPI Getting Started](../openapi/getting-started) — annotate your first endpoint
-- [Runtime Builder DSL](../advanced/runtime-builder) — build or extend specs programmatically at runtime
+- [Javalin Swagger UI](./swagger) - interactive API explorer
+- [Javalin ReDoc](./redoc) - clean API reference
+- [OpenAPI Getting Started](../openapi/getting-started) - annotate your first endpoint
+- [Runtime Builder DSL](../advanced/runtime-builder) - build or extend specs programmatically at runtime
diff --git a/docs/introduction/swagger.md b/docs/introduction/swagger.md
index 0b3911c3..d615d13c 100644
--- a/docs/introduction/swagger.md
+++ b/docs/introduction/swagger.md
@@ -57,4 +57,4 @@ When running behind a reverse proxy that adds a base path, set `basePath` so the
swagger.basePath = "/api"
```
-`SwaggerPlugin` is repeatable — you can register multiple instances for different API versions or configurations.
+`SwaggerPlugin` is repeatable - you can register multiple instances for different API versions or configurations.
diff --git a/docs/json-schema/custom-properties.md b/docs/json-schema/custom-properties.md
index 749172f4..939b864f 100644
--- a/docs/json-schema/custom-properties.md
+++ b/docs/json-schema/custom-properties.md
@@ -14,7 +14,7 @@ class InternalService {
}
```
-`@Custom` is repeatable — you can add multiple custom properties to the same element.
+`@Custom` is repeatable - you can add multiple custom properties to the same element.
## @CustomAnnotation
diff --git a/docs/json-schema/getting-started.md b/docs/json-schema/getting-started.md
index 9e9da6bb..2aa3128f 100644
--- a/docs/json-schema/getting-started.md
+++ b/docs/json-schema/getting-started.md
@@ -35,7 +35,7 @@ At compile time, this produces a `/json-schemes/com.example.ServerConfig` resour
}
```
-All nested types are inlined directly — there are no `$ref` references.
+All nested types are inlined directly - there are no `$ref` references.
## Loading at Runtime
@@ -75,12 +75,12 @@ class InternalEntity
All property-level `@OpenApi*` annotations also apply to JSON Schema output:
-- `@OpenApiName` — override property names
-- `@OpenApiDescription` — add descriptions
-- `@OpenApiIgnore` — exclude properties
-- `@OpenApiRequired` — force required
-- `@OpenApiPropertyType` — override types
-- `@OpenApiNullable` — mark nullable
-- `@OpenApiNaming` — naming strategies (snake_case, kebab-case)
-- `@OpenApiByFields` — resolve from fields instead of getters
-- `@OpenApiNumberValidation`, `@OpenApiStringValidation`, `@OpenApiArrayValidation`, `@OpenApiObjectValidation` — validation constraints
+- `@OpenApiName` - override property names
+- `@OpenApiDescription` - add descriptions
+- `@OpenApiIgnore` - exclude properties
+- `@OpenApiRequired` - force required
+- `@OpenApiPropertyType` - override types
+- `@OpenApiNullable` - mark nullable
+- `@OpenApiNaming` - naming strategies (snake_case, kebab-case)
+- `@OpenApiByFields` - resolve from fields instead of getters
+- `@OpenApiNumberValidation`, `@OpenApiStringValidation`, `@OpenApiArrayValidation`, `@OpenApiObjectValidation` - validation constraints
diff --git a/docs/openapi/enums.md b/docs/openapi/enums.md
index 4f019dc6..843fe2d3 100644
--- a/docs/openapi/enums.md
+++ b/docs/openapi/enums.md
@@ -44,9 +44,9 @@ Apply `@OpenApiNaming` to transform all value names automatically:
```kotlin
@OpenApiNaming(OpenApiNamingStrategy.KEBAB_CASE)
enum class ErrorCode {
- NOT_FOUND, // → "not-found"
- BAD_REQUEST, // → "bad-request"
- SERVER_ERROR // → "server-error"
+ NOT_FOUND, // -> "not-found"
+ BAD_REQUEST, // -> "bad-request"
+ SERVER_ERROR // -> "server-error"
}
```
diff --git a/docs/openapi/naming.md b/docs/openapi/naming.md
index eaefee7e..7de58216 100644
--- a/docs/openapi/naming.md
+++ b/docs/openapi/naming.md
@@ -9,9 +9,9 @@ Apply `@OpenApiNaming` on a class to transform all property names:
```kotlin
@OpenApiNaming(OpenApiNamingStrategy.SNAKE_CASE)
class UserProfile {
- val firstName: String = "" // → "first_name"
- val lastName: String = "" // → "last_name"
- val emailAddress: String = "" // → "email_address"
+ val firstName: String = "" // -> "first_name"
+ val lastName: String = "" // -> "last_name"
+ val emailAddress: String = "" // -> "email_address"
}
```
@@ -30,10 +30,10 @@ class UserProfile {
```kotlin
@OpenApiNaming(OpenApiNamingStrategy.SNAKE_CASE)
class UserProfile {
- val firstName: String = "" // → "first_name" (from strategy)
+ val firstName: String = "" // -> "first_name" (from strategy)
@OpenApiName("ID")
- val id: String = "" // → "ID" (explicit override)
+ val id: String = "" // -> "ID" (explicit override)
}
```
@@ -44,9 +44,9 @@ class UserProfile {
```kotlin
@OpenApiNaming(OpenApiNamingStrategy.KEBAB_CASE)
enum class StatusCode {
- NOT_FOUND, // → "not-found"
- BAD_REQUEST, // → "bad-request"
- INTERNAL_ERROR // → "internal-error"
+ NOT_FOUND, // -> "not-found"
+ BAD_REQUEST, // -> "bad-request"
+ INTERNAL_ERROR // -> "internal-error"
}
```
@@ -55,12 +55,12 @@ Use `@OpenApiName` on individual values to override specific entries:
```kotlin
@OpenApiNaming(OpenApiNamingStrategy.KEBAB_CASE)
enum class StatusCode {
- NOT_FOUND, // → "not-found"
+ NOT_FOUND, // -> "not-found"
@OpenApiName("custom-value")
- BAD_REQUEST, // → "custom-value"
+ BAD_REQUEST, // -> "custom-value"
- INTERNAL_ERROR // → "internal-error"
+ INTERNAL_ERROR // -> "internal-error"
}
```
diff --git a/docs/openapi/parameters.md b/docs/openapi/parameters.md
index f3aa9813..100b4eec 100644
--- a/docs/openapi/parameters.md
+++ b/docs/openapi/parameters.md
@@ -83,9 +83,9 @@ Use `@OpenApiParam` to describe path, query, header, and cookie parameters.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
-| `name` | `String` | — | Parameter name (required) |
+| `name` | `String` | - | Parameter name (required) |
| `type` | `KClass<*>` | `String::class` | Parameter type |
-| `description` | `String` | — | Description |
+| `description` | `String` | - | Description |
| `deprecated` | `Boolean` | `false` | Mark as deprecated |
| `required` | `Boolean` | `false` | Mark as required |
| `allowEmptyValue` | `Boolean` | `false` | Allow empty values |
diff --git a/docs/openapi/request-body.md b/docs/openapi/request-body.md
index 60c7f4be..1a88ab31 100644
--- a/docs/openapi/request-body.md
+++ b/docs/openapi/request-body.md
@@ -52,9 +52,9 @@ Use `@OpenApiRequestBody` and `@OpenApiContent` to describe request bodies.
When `mimeType` is not specified, the content type is auto-detected from the `from` type:
-- Object types → `application/json`
-- `String` → `text/plain`
-- `ByteArray`, `InputStream`, `File` → `application/octet-stream`
+- Object types -> `application/json`
+- `String` -> `text/plain`
+- `ByteArray`, `InputStream`, `File` -> `application/octet-stream`
## Inline Properties
@@ -109,11 +109,11 @@ Generates:
| Property | Type | Default | Description |
|----------|------|---------|-------------|
-| `from` | `KClass<*>` | — | Schema type |
+| `from` | `KClass<*>` | - | Schema type |
| `mimeType` | `String` | Auto-detect | Content type |
-| `type` | `String` | — | Override type |
-| `format` | `String` | — | Override format |
+| `type` | `String` | - | Override type |
+| `format` | `String` | - | Override format |
| `properties` | `OpenApiContentProperty[]` | `[]` | Inline properties |
-| `additionalProperties` | `OpenApiAdditionalContent` | — | Map value type |
-| `example` | `String` | — | Example value |
+| `additionalProperties` | `OpenApiAdditionalContent` | - | Map value type |
+| `example` | `String` | - | Example value |
| `exampleObjects` | `OpenApiExampleProperty[]` | `[]` | Structured example |
diff --git a/docs/openapi/responses.md b/docs/openapi/responses.md
index e5cd56af..16738df6 100644
--- a/docs/openapi/responses.md
+++ b/docs/openapi/responses.md
@@ -74,7 +74,7 @@ OpenApiResponse(
| Property | Type | Default | Description |
|----------|------|---------|-------------|
-| `status` | `String` | — | HTTP status code (required) |
+| `status` | `String` | - | HTTP status code (required) |
| `content` | `OpenApiContent[]` | `[]` | Response content |
| `description` | `String` | `""` | Response description |
| `headers` | `OpenApiParam[]` | `[]` | Response headers |
diff --git a/docs/openapi/schemas.md b/docs/openapi/schemas.md
index 1541084c..0d94e6fd 100644
--- a/docs/openapi/schemas.md
+++ b/docs/openapi/schemas.md
@@ -8,17 +8,17 @@ The annotation processor automatically generates OpenAPI component schemas from
| Java Type | OpenAPI Type | Format |
|-----------|-------------|--------|
-| `boolean` / `Boolean` | `boolean` | — |
+| `boolean` / `Boolean` | `boolean` | - |
| `byte` / `Byte` | `integer` | `int32` |
| `short` / `Short` | `integer` | `int32` |
| `int` / `Integer` | `integer` | `int32` |
| `long` / `Long` | `integer` | `int64` |
| `float` / `Float` | `number` | `float` |
| `double` / `Double` | `number` | `double` |
-| `char` / `Character` | `string` | — |
-| `String` | `string` | — |
-| `BigDecimal` | `string` | — |
-| `BigInteger` | `integer` | — |
+| `char` / `Character` | `string` | - |
+| `String` | `string` | - |
+| `BigDecimal` | `string` | - |
+| `BigInteger` | `integer` | - |
| `UUID` | `string` | `uuid` |
### Date & Time Types
@@ -65,8 +65,8 @@ The annotation processor automatically generates OpenAPI component schemas from
By default, properties are resolved from getter methods following JavaBean conventions. The `get` / `is` prefix is stripped:
-- `getName()` → `name`
-- `isActive()` → `active`
+- `getName()` -> `name`
+- `isActive()` -> `active`
### Java Records
@@ -86,6 +86,8 @@ class Config {
The `value` parameter controls the minimum field visibility. Use `only = true` to ignore methods entirely.
+`only = true` is supported by the APT/Kapt backend. KSP does not expose the Java-style field model needed for this mode yet, so the KSP backend fails the build with a clear error if it sees `@OpenApiByFields(only = true)`.
+
## Property Annotations
### @OpenApiIgnore
@@ -175,7 +177,7 @@ Generic type parameters are resolved when used in concrete contexts. `Page
## Custom Type Mappings
-Register custom type mappings in the [compile-time configuration](../advanced/configuration):
+Register custom type mappings in the [scripting configuration](../advanced/scripting):
```groovy
configuration.simpleTypeMappings['org.bson.types.ObjectId'] = new SimpleType("string")
diff --git a/examples/javalin-gradle-kotlin/src/main/compile/openapi.groovy b/examples/javalin-gradle-kotlin/src/main/compile/openapi.groovy
index 548cfcae..2d3b04ed 100644
--- a/examples/javalin-gradle-kotlin/src/main/compile/openapi.groovy
+++ b/examples/javalin-gradle-kotlin/src/main/compile/openapi.groovy
@@ -7,7 +7,6 @@ class OpenApiConfiguration implements OpenApiAnnotationProcessorConfigurer {
@Override
void configure(OpenApiAnnotationProcessorConfiguration openApiAnnotationProcessorConfiguration) {
- // openApiAnnotationProcessorConfiguration.debug = true
}
-}
\ No newline at end of file
+}
diff --git a/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java b/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java
index 519d4a81..8fca8b89 100644
--- a/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java
+++ b/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java
@@ -45,7 +45,7 @@ enum Rules implements RouteRole {
* @param args args
*/
public static void main(String[] args) {
- var app = Javalin.create(config -> {
+ Javalin.start(config -> {
// config.routing.contextPath = "/custom";
String deprecatedDocsPath = "/api/openapi.json"; // by default it's /openapi
@@ -110,7 +110,6 @@ public static void main(String[] args) {
System.out.println(generatedJsonSchema.getContentAsString());
}
});
- app.start();
}
@OpenApi(
@@ -217,7 +216,6 @@ public static void main(String[] args) {
@OpenApiContent(from = EntityDto[].class), // array
@OpenApiContent(from = File.class), // file
@OpenApiContent(type = "application/json"), // empty
- @OpenApiContent(), // empty
@OpenApiContent(mimeType = "image/png", type = "string", format = "base64"), // single file upload,
@OpenApiContent(mimeType = "multipart/form-data", properties = {
@OpenApiContentProperty(name = "form-element", type = "integer"), // random element in form-data
diff --git a/examples/javalin-ksp-kotlin/build.gradle.kts b/examples/javalin-ksp-kotlin/build.gradle.kts
new file mode 100644
index 00000000..dfff3311
--- /dev/null
+++ b/examples/javalin-ksp-kotlin/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+ alias(libs.plugins.ksp)
+}
+
+dependencies {
+ ksp(project(":openapi-ksp"))
+ implementation(project(":javalin-plugins:javalin-openapi-plugin"))
+ implementation(project(":javalin-plugins:javalin-swagger-plugin"))
+ implementation(project(":javalin-plugins:javalin-redoc-plugin"))
+
+ implementation(libs.javalin)
+ implementation(libs.jackson.databind)
+ implementation(libs.logback.classic)
+}
+
+ksp {
+ arg("openapi.info.title", "Awesome KSP App")
+ arg("openapi.info.version", "1.0.0")
+}
+
+application {
+ mainClass.set("io.javalin.openapi.ksp.example.AppKt")
+}
+
+repositories {
+ mavenCentral()
+}
diff --git a/examples/javalin-ksp-kotlin/src/main/kotlin/io/javalin/openapi/ksp/example/App.kt b/examples/javalin-ksp-kotlin/src/main/kotlin/io/javalin/openapi/ksp/example/App.kt
new file mode 100644
index 00000000..b941298f
--- /dev/null
+++ b/examples/javalin-ksp-kotlin/src/main/kotlin/io/javalin/openapi/ksp/example/App.kt
@@ -0,0 +1,51 @@
+package io.javalin.openapi.ksp.example
+
+import io.javalin.Javalin
+import io.javalin.http.Context
+import io.javalin.http.Handler
+import io.javalin.openapi.HttpMethod
+import io.javalin.openapi.OpenApi
+import io.javalin.openapi.OpenApiContent
+import io.javalin.openapi.OpenApiResponse
+import io.javalin.openapi.OpenApiStatus
+import io.javalin.openapi.plugin.OpenApiPlugin
+import io.javalin.openapi.plugin.redoc.ReDocPlugin
+import io.javalin.openapi.plugin.swagger.SwaggerPlugin
+
+data class Account(
+ val id: String,
+ val age: Int,
+ val roles: List,
+)
+
+class AccountHandler : Handler {
+ @OpenApi(
+ path = "/account",
+ methods = [HttpMethod.GET],
+ summary = "Get the current account",
+ operationId = "getAccount",
+ responses = [
+ OpenApiResponse(
+ status = OpenApiStatus.OK,
+ content = [OpenApiContent(from = Account::class)],
+ )
+ ],
+ )
+ override fun handle(ctx: Context) {
+ ctx.json(Account(id = "u-1", age = 30, roles = listOf("admin")))
+ }
+}
+
+fun main() {
+ Javalin.start { config ->
+ config.registerPlugin(OpenApiPlugin {})
+ config.registerPlugin(SwaggerPlugin {})
+ config.registerPlugin(ReDocPlugin {})
+
+ config.routes.get("/account", AccountHandler())
+ }
+
+ println("OpenAPI document: http://localhost:8080/openapi")
+ println("Swagger UI: http://localhost:8080/swagger")
+ println("ReDoc: http://localhost:8080/redoc")
+}
diff --git a/examples/javalin-ksp-kotlin/src/main/resources/logback.xml b/examples/javalin-ksp-kotlin/src/main/resources/logback.xml
new file mode 100644
index 00000000..776c8b44
--- /dev/null
+++ b/examples/javalin-ksp-kotlin/src/main/resources/logback.xml
@@ -0,0 +1,11 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
+
+
+
+
+
+
+
diff --git a/examples/javalin-maven-java/pom.xml b/examples/javalin-maven-java/pom.xml
index 9d9cf41e..c2863a96 100644
--- a/examples/javalin-maven-java/pom.xml
+++ b/examples/javalin-maven-java/pom.xml
@@ -12,7 +12,7 @@
11
11
7.2.2
- 7.2.2
+ 7.3.0-RC.1
2.1.0
diff --git a/examples/javalin-maven-java/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java b/examples/javalin-maven-java/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java
index 1eb364f7..23f2a52c 100644
--- a/examples/javalin-maven-java/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java
+++ b/examples/javalin-maven-java/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java
@@ -41,7 +41,7 @@ public final class JavalinTest implements Handler {
* @param args args
*/
public static void main(String[] args) {
- Javalin.create(config -> {
+ Javalin.start(config -> {
String deprecatedDocsPath = "/swagger-docs";
config.registerPlugin(new OpenApiPlugin(openApiConfig ->
@@ -101,8 +101,7 @@ public static void main(String[] args) {
config.registerPlugin(new ReDocPlugin(reDocConfiguration ->
reDocConfiguration.documentationPath = deprecatedDocsPath
));
- })
- .start(8080);
+ });
}
private static final String ROUTE = "/main/{name}";
diff --git a/examples/javalin-maven-kotlin/pom.xml b/examples/javalin-maven-kotlin/pom.xml
index 0ff2be0d..040cf91d 100644
--- a/examples/javalin-maven-kotlin/pom.xml
+++ b/examples/javalin-maven-kotlin/pom.xml
@@ -10,7 +10,7 @@
7.2.2
- 7.2.2
+ 7.3.0-RC.1
2.1.0
diff --git a/examples/javalin-maven-kotlin/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinTest.kt b/examples/javalin-maven-kotlin/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinTest.kt
index 87244489..ac6be15c 100644
--- a/examples/javalin-maven-kotlin/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinTest.kt
+++ b/examples/javalin-maven-kotlin/src/main/kotlin/io/javalin/openapi/plugin/test/KotlinTest.kt
@@ -14,7 +14,7 @@ import io.javalin.openapi.plugin.swagger.SwaggerPlugin
path = "/"
)
fun main() {
- Javalin.createAndStart { config ->
+ Javalin.start { config ->
config.registerPlugin(
OpenApiPlugin {
it.documentationPath = "/openapi"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 47c3ccac..b487c2bd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -22,6 +22,8 @@ assertj = "3.27.6"
lombok = "1.18.42"
json-unit = "4.1.1"
unirest = "3.14.5"
+ksp = "2.3.9"
+kctfork = "0.12.1"
[libraries]
# Javalin
@@ -40,6 +42,11 @@ jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" }
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
+# KSP (introspection-ksp backend)
+ksp-symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
+kctfork-core = { module = "dev.zacsweers.kctfork:core", version.ref = "kctfork" }
+kctfork-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kctfork" }
+
# Other
groovy = { module = "org.apache.groovy:groovy", version.ref = "groovy" }
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
@@ -60,4 +67,5 @@ unirest = { module = "com.konghq:unirest-java", version.ref = "unirest" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus-publish" }
\ No newline at end of file
diff --git a/introspection/introspection-api/build.gradle.kts b/introspection/introspection-api/build.gradle.kts
new file mode 100644
index 00000000..845f6a3c
--- /dev/null
+++ b/introspection/introspection-api/build.gradle.kts
@@ -0,0 +1,8 @@
+description = "Introspection API | Backend-agnostic type & symbol introspection model"
+
+dependencies {
+ testImplementation(libs.junit.jupiter.api)
+ testImplementation(libs.junit.jupiter.engine)
+ testRuntimeOnly(libs.junit.platform.launcher)
+ testImplementation(libs.assertj.core)
+}
diff --git a/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/AnnotationSet.kt b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/AnnotationSet.kt
new file mode 100644
index 00000000..33b66ec7
--- /dev/null
+++ b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/AnnotationSet.kt
@@ -0,0 +1,60 @@
+package io.javalin.introspection
+
+interface AnnotationSet {
+
+ fun all(): List
+
+ fun find(type: Class): AnnotationProjection?
+
+ fun findAll(type: Class): List
+
+ fun contains(simpleName: String): Boolean
+
+ fun contains(type: Class): Boolean = find(type) != null
+
+}
+
+interface AnnotationProjection {
+
+ val simpleName: String
+
+ val metadata: AnnotationSet
+
+ val values: Map
+
+ operator fun get(member: String): AnnotationValue = AnnotationValue(values[member])
+
+}
+
+data class AnnotationValue(private val value: Any?) {
+
+ fun raw(): Any? = value
+
+ fun asString(): String? = value as? String
+
+ fun asBoolean(): Boolean? = value as? Boolean
+
+ fun asClassDefinition(): ClassDefinition? = value as? ClassDefinition
+
+ fun asClassDefinitions(): List =
+ asList().filterIsInstance()
+
+ fun asList(): List<*> = value as? List<*> ?: emptyList()
+
+ fun asMap(): Map<*, *>? = value as? Map<*, *>
+
+}
+
+class RepeatableAnnotationProjection(
+ override val simpleName: String,
+ override val values: Map,
+) : AnnotationProjection {
+ override val metadata: AnnotationSet = EmptyAnnotationSet
+}
+
+private object EmptyAnnotationSet : AnnotationSet {
+ override fun all(): List = emptyList()
+ override fun find(type: Class): AnnotationProjection? = null
+ override fun findAll(type: Class): List = emptyList()
+ override fun contains(simpleName: String): Boolean = false
+}
diff --git a/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/ClassDefinition.kt b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/ClassDefinition.kt
new file mode 100644
index 00000000..69a52214
--- /dev/null
+++ b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/ClassDefinition.kt
@@ -0,0 +1,37 @@
+package io.javalin.introspection
+
+abstract class ClassDefinition(
+ val simpleName: String,
+ val fullName: String,
+ val generics: List = emptyList(),
+ val structureType: StructureType = StructureType.DEFAULT,
+) {
+
+ @InternalIntrospectionApi
+ abstract val source: Any
+
+ abstract fun isEnum(): Boolean
+
+ abstract fun getEnumConstants(): List
+
+ abstract fun getProperties(): List
+
+ abstract fun getAnnotations(): AnnotationSet
+
+ override fun toString(): String =
+ when {
+ generics.isEmpty() -> fullName
+ else -> "$fullName<${generics.joinToString(", ")}>"
+ }
+}
+
+enum class StructureType {
+ DEFAULT,
+ ARRAY,
+ DICTIONARY,
+}
+
+data class EnumConstant(
+ val name: String,
+ val annotations: AnnotationSet,
+)
diff --git a/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/CompileTimeIntrospector.kt b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/CompileTimeIntrospector.kt
new file mode 100644
index 00000000..94bf4dc0
--- /dev/null
+++ b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/CompileTimeIntrospector.kt
@@ -0,0 +1,10 @@
+package io.javalin.introspection
+
+interface CompileTimeIntrospector : TypeIntrospector {
+
+ fun typesAnnotatedWith(
+ annotationType: Class,
+ assignableTo: ClassDefinition? = null,
+ ): List
+
+}
diff --git a/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/InternalIntrospectionApi.kt b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/InternalIntrospectionApi.kt
new file mode 100644
index 00000000..10b442fb
--- /dev/null
+++ b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/InternalIntrospectionApi.kt
@@ -0,0 +1,9 @@
+package io.javalin.introspection
+
+@RequiresOptIn(
+ level = RequiresOptIn.Level.ERROR,
+ message = "Target agnostic code must not touch that part of the API.",
+)
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.PROPERTY)
+annotation class InternalIntrospectionApi
diff --git a/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/PropertyProjection.kt b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/PropertyProjection.kt
new file mode 100644
index 00000000..6b28e6d4
--- /dev/null
+++ b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/PropertyProjection.kt
@@ -0,0 +1,26 @@
+package io.javalin.introspection
+
+class PropertyProjection(
+ val name: String,
+ val type: ClassDefinition,
+ val accessor: Accessor,
+ val nullable: Boolean,
+ val visibility: MemberVisibility,
+ val transient: Boolean,
+ val annotations: AnnotationSet,
+ @property:InternalIntrospectionApi val source: Any,
+)
+
+enum class Accessor { FIELD, GETTER, RECORD_COMPONENT }
+
+enum class MemberVisibility { PUBLIC, PROTECTED, PACKAGE_PRIVATE, PRIVATE }
+
+fun isGetterName(name: String): Boolean =
+ (name.startsWith("get") && name.length > 3 && name[3].isUpperCase()) ||
+ (name.startsWith("is") && name.length > 2 && name[2].isUpperCase())
+
+fun propertyName(getterName: String): String =
+ when {
+ getterName.startsWith("get") -> getterName.removePrefix("get")
+ else -> getterName.removePrefix("is")
+ }.replaceFirstChar { it.lowercase() }
diff --git a/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/TypeIntrospector.kt b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/TypeIntrospector.kt
new file mode 100644
index 00000000..d501952d
--- /dev/null
+++ b/introspection/introspection-api/src/main/kotlin/io/javalin/introspection/TypeIntrospector.kt
@@ -0,0 +1,5 @@
+package io.javalin.introspection
+
+fun interface TypeIntrospector {
+ fun introspect(source: Any): ClassDefinition
+}
diff --git a/introspection/introspection-jap/build.gradle.kts b/introspection/introspection-jap/build.gradle.kts
new file mode 100644
index 00000000..2b5494c4
--- /dev/null
+++ b/introspection/introspection-jap/build.gradle.kts
@@ -0,0 +1,10 @@
+description = "Introspection JAP | javax.lang.model (Java annotation processing) TypeIntrospector backend"
+
+dependencies {
+ api(project(":introspection:introspection-api"))
+
+ testImplementation(libs.junit.jupiter.api)
+ testImplementation(libs.junit.jupiter.engine)
+ testRuntimeOnly(libs.junit.platform.launcher)
+ testImplementation(libs.assertj.core)
+}
diff --git a/introspection/introspection-jap/src/main/kotlin/io/javalin/introspection/jap/JapTypeIntrospector.kt b/introspection/introspection-jap/src/main/kotlin/io/javalin/introspection/jap/JapTypeIntrospector.kt
new file mode 100644
index 00000000..56c2a102
--- /dev/null
+++ b/introspection/introspection-jap/src/main/kotlin/io/javalin/introspection/jap/JapTypeIntrospector.kt
@@ -0,0 +1,455 @@
+package io.javalin.introspection.jap
+
+import io.javalin.introspection.Accessor
+import io.javalin.introspection.AnnotationProjection
+import io.javalin.introspection.AnnotationSet
+import io.javalin.introspection.ClassDefinition
+import io.javalin.introspection.CompileTimeIntrospector
+import io.javalin.introspection.EnumConstant
+import io.javalin.introspection.InternalIntrospectionApi
+import io.javalin.introspection.MemberVisibility
+import io.javalin.introspection.PropertyProjection
+import io.javalin.introspection.RepeatableAnnotationProjection
+import io.javalin.introspection.StructureType
+import io.javalin.introspection.StructureType.ARRAY
+import io.javalin.introspection.StructureType.DEFAULT
+import io.javalin.introspection.StructureType.DICTIONARY
+import io.javalin.introspection.isGetterName
+import io.javalin.introspection.propertyName
+import java.lang.annotation.Inherited
+import java.lang.annotation.Repeatable as JavaRepeatable
+import javax.annotation.processing.RoundEnvironment
+import javax.lang.model.element.AnnotationMirror
+import javax.lang.model.element.AnnotationValue
+import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
+import javax.lang.model.element.ExecutableElement
+import javax.lang.model.element.Modifier
+import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
+import javax.lang.model.type.ArrayType
+import javax.lang.model.type.DeclaredType
+import javax.lang.model.type.PrimitiveType
+import javax.lang.model.type.TypeKind
+import javax.lang.model.type.TypeMirror
+import javax.lang.model.type.TypeVariable
+import javax.lang.model.type.WildcardType
+import javax.lang.model.util.Elements
+import javax.lang.model.util.SimpleAnnotationValueVisitor8
+import javax.lang.model.util.Types
+
+class JapTypeIntrospector(
+ private val types: Types,
+ private val elements: Elements,
+ private val roundEnvProvider: () -> RoundEnvironment? = { null },
+) : CompileTimeIntrospector {
+
+ override fun introspect(source: Any): ClassDefinition {
+ require(source is TypeMirror) { "JapTypeIntrospector expects a javax.lang.model.type.TypeMirror, got ${source::class.java.name}" }
+ return resolve(source)
+ }
+
+ fun annotationsOf(element: Element): AnnotationSet =
+ AnnotationsView(listOf(element), includeInherited = element is TypeElement)
+
+ @OptIn(InternalIntrospectionApi::class)
+ override fun typesAnnotatedWith(annotationType: Class, assignableTo: ClassDefinition?): List {
+ val roundEnv = roundEnvProvider() ?: return emptyList()
+ val target = assignableTo?.source as? TypeMirror
+ return roundEnv.getElementsAnnotatedWith(annotationType)
+ .filterIsInstance()
+ .filter { target == null || types.isAssignable(it.asType(), target) }
+ .map { resolve(it.asType()) }
+ }
+
+ private fun resolve(
+ mirror: TypeMirror,
+ generics: List = emptyList(),
+ structureType: StructureType = DEFAULT,
+ visitingTypeVariables: Set = emptySet(),
+ ): ClassDefinition =
+ when (mirror) {
+ is TypeVariable -> {
+ val key = mirror.asElement()?.toString() ?: mirror.toString()
+ val bound = mirror.upperBound ?: mirror.lowerBound
+ when {
+ key in visitingTypeVariables -> objectDefinition(structureType)
+ bound != null ->
+ resolve(
+ mirror = bound,
+ generics = generics,
+ structureType = structureType,
+ visitingTypeVariables = visitingTypeVariables + key,
+ )
+ else -> objectDefinition(structureType)
+ }
+ }
+ is WildcardType ->
+ resolve(
+ mirror = mirror.extendsBound ?: objectMirror(),
+ generics = generics,
+ structureType = structureType,
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ is ArrayType -> resolve(
+ mirror = mirror.componentType,
+ generics = generics,
+ structureType = ARRAY,
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ is PrimitiveType -> definition(
+ mirror = types.boxedClass(mirror).asType(),
+ generics = generics,
+ structureType = structureType,
+ )
+ is DeclaredType -> declared(
+ mirror = mirror,
+ generics = generics,
+ structureType = structureType,
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ else ->
+ types
+ .asElement(mirror)
+ ?.asType()
+ ?.takeIf { it != mirror }
+ ?.let {
+ resolve(
+ mirror = it,
+ generics = generics,
+ structureType = structureType,
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ }
+ ?: objectDefinition(structureType)
+ }
+
+ private fun declared(
+ mirror: DeclaredType,
+ generics: List,
+ structureType: StructureType,
+ visitingTypeVariables: Set,
+ ): ClassDefinition {
+ val erasure = types.erasure(mirror)
+ return when {
+ types.isAssignable(erasure, erasureOf("java.util.Map")) -> {
+ val keyType = resolve(
+ mirror.typeArguments.getOrElse(0) { objectMirror() },
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ val valueType = resolve(
+ mirror.typeArguments.getOrElse(1) { objectMirror() },
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ definition(
+ mirror = mirror,
+ generics = listOf(keyType, valueType),
+ structureType = DICTIONARY,
+ )
+ }
+ types.isAssignable(erasure, erasureOf("java.util.Collection")) ->
+ resolve(
+ mirror = mirror.typeArguments.getOrElse(0) { objectMirror() },
+ generics = generics,
+ structureType = ARRAY,
+ visitingTypeVariables = visitingTypeVariables,
+ )
+ else ->
+ definition(
+ mirror = mirror,
+ generics = mirror.typeArguments.map { resolve(it, visitingTypeVariables = visitingTypeVariables) },
+ structureType = structureType,
+ )
+ }
+ }
+
+ private fun definition(
+ mirror: TypeMirror,
+ generics: List,
+ structureType: StructureType,
+ sourceMirror: TypeMirror = mirror,
+ ): ClassDefinition {
+ val element = types.asElement(mirror) as? TypeElement
+ val fullName = element?.qualifiedName?.toString() ?: mirror.toString().substringBefore("<")
+
+ return Definition(
+ simpleName = element?.simpleName?.toString() ?: fullName.substringAfterLast('.'),
+ fullName = fullName,
+ generics = generics,
+ structureType = structureType,
+ mirror = mirror,
+ sourceMirror = sourceMirror,
+ )
+ }
+
+ private fun primitiveDefinition(mirror: PrimitiveType): ClassDefinition =
+ definition(
+ mirror = types.boxedClass(mirror).asType(),
+ generics = emptyList(),
+ structureType = DEFAULT,
+ sourceMirror = mirror,
+ )
+
+ private fun objectDefinition(structureType: StructureType = DEFAULT): ClassDefinition =
+ definition(
+ mirror = objectMirror(),
+ generics = emptyList(),
+ structureType = structureType,
+ )
+
+ private fun objectMirror(): TypeMirror =
+ elements.getTypeElement(Any::class.java.name).asType()
+
+ private fun erasureOf(name: String): TypeMirror =
+ types.erasure(elements.getTypeElement(name).asType())
+
+ private val annotationMirrorValueVisitor = object : SimpleAnnotationValueVisitor8() {
+ override fun defaultAction(value: Any?, p: Nothing?): Any? = value
+
+ override fun visitType(type: TypeMirror, p: Nothing?): Any =
+ when {
+ type is PrimitiveType -> primitiveDefinition(type)
+ else -> resolve(type)
+ }
+
+ override fun visitEnumConstant(constant: VariableElement, p: Nothing?): Any =
+ constant.simpleName.toString()
+
+ override fun visitArray(values: MutableList, p: Nothing?): Any =
+ values.map { it.accept(this, null) }
+
+ override fun visitAnnotation(annotation: AnnotationMirror, p: Nothing?): Any =
+ mirrorValues(annotation)
+ }
+
+ private fun mirrorValues(mirror: AnnotationMirror): Map =
+ elements
+ .getElementValuesWithDefaults(mirror)
+ .entries
+ .associate { (member, value) -> member.simpleName.toString() to value.accept(annotationMirrorValueVisitor, null) }
+
+ private inner class Definition(
+ simpleName: String,
+ fullName: String,
+ generics: List,
+ structureType: StructureType,
+ private val mirror: TypeMirror,
+ private val sourceMirror: TypeMirror = mirror,
+ ) : ClassDefinition(
+ simpleName = simpleName,
+ fullName = fullName,
+ generics = generics,
+ structureType = structureType,
+ ) {
+
+ @InternalIntrospectionApi
+ override val source: Any
+ get() = sourceMirror
+
+ override fun isEnum(): Boolean =
+ typeElement()?.kind == ElementKind.ENUM
+
+ override fun getEnumConstants(): List =
+ typeElement()
+ ?.takeIf { it.kind == ElementKind.ENUM }
+ ?.enclosedElements
+ ?.filter { it.kind == ElementKind.ENUM_CONSTANT }
+ ?.map {
+ EnumConstant(
+ name = it.simpleName.toString(),
+ annotations = AnnotationsView(sources = listOf(it)),
+ )
+ }
+ .orEmpty()
+
+ override fun getAnnotations(): AnnotationSet =
+ AnnotationsView(
+ sources = listOfNotNull(typeElement()),
+ includeInherited = true,
+ )
+
+ override fun getProperties(): List {
+ val element = typeElement() ?: return emptyList()
+
+ if (element.kind == ElementKind.RECORD) {
+ val recordProperties = element.recordComponents.map { component ->
+ PropertyProjection(
+ name = component.simpleName.toString(),
+ type = resolve(component.asType()),
+ accessor = Accessor.RECORD_COMPONENT,
+ nullable = component.asType().nullable(),
+ visibility = MemberVisibility.PUBLIC,
+ transient = false,
+ source = component,
+ annotations = AnnotationsView(listOf(component)),
+ )
+ }
+ val recordPropertyNames = recordProperties.mapTo(mutableSetOf()) { it.name }
+ val extraGetters = elements.getAllMembers(element).mapNotNull { member ->
+ if (!member.isGetter()) {
+ return@mapNotNull null
+ }
+
+ val getter = member as ExecutableElement
+ val name = propertyName(getter.simpleName.toString())
+ if (name in recordPropertyNames) {
+ return@mapNotNull null
+ }
+
+ PropertyProjection(
+ name = name,
+ type = resolve(getter.returnType),
+ accessor = Accessor.GETTER,
+ nullable = getter.returnType.nullable(),
+ visibility = getter.visibility(),
+ transient = false,
+ source = getter,
+ annotations = AnnotationsView(listOf(getter)),
+ )
+ }
+
+ return recordProperties + extraGetters
+ }
+
+ return elements.getAllMembers(element).mapNotNull { member ->
+ when {
+ member.isGetter() -> (member as ExecutableElement).let { getter ->
+ PropertyProjection(
+ name = propertyName(getter.simpleName.toString()),
+ type = resolve(getter.returnType),
+ accessor = Accessor.GETTER,
+ nullable = getter.returnType.nullable(),
+ visibility = getter.visibility(),
+ transient = false,
+ source = getter,
+ annotations = AnnotationsView(listOf(getter)),
+ )
+ }
+ member.isInstanceField() -> (member as VariableElement).let { field ->
+ PropertyProjection(
+ name = field.simpleName.toString(),
+ type = resolve(field.asType()),
+ accessor = Accessor.FIELD,
+ nullable = field.asType().nullable(),
+ visibility = field.visibility(),
+ transient = Modifier.TRANSIENT in field.modifiers,
+ source = field,
+ annotations = AnnotationsView(listOf(field)),
+ )
+ }
+ else -> null
+ }
+ }
+ }
+
+ private fun typeElement(): TypeElement? =
+ types.asElement(mirror) as? TypeElement
+
+ private fun TypeMirror.nullable(): Boolean =
+ !kind.isPrimitive
+
+ private fun Element.isGetter(): Boolean =
+ when {
+ kind != ElementKind.METHOD || this !is ExecutableElement -> false
+ Modifier.STATIC in modifiers -> false
+ parameters.isNotEmpty() || enclosingElement.toString() == Any::class.java.name -> false
+ returnType.kind == TypeKind.VOID -> false
+ else ->
+ isGetterName(simpleName.toString()) ||
+ annotationMirrors.any { it.annotationType.asElement().simpleName.contentEquals("OpenApiName") }
+ }
+
+ private fun Element.isInstanceField(): Boolean =
+ kind == ElementKind.FIELD && this is VariableElement && Modifier.STATIC !in modifiers
+
+ private fun Element.visibility(): MemberVisibility =
+ when {
+ Modifier.PUBLIC in modifiers -> MemberVisibility.PUBLIC
+ Modifier.PROTECTED in modifiers -> MemberVisibility.PROTECTED
+ Modifier.PRIVATE in modifiers -> MemberVisibility.PRIVATE
+ else -> MemberVisibility.PACKAGE_PRIVATE
+ }
+ }
+
+ private inner class AnnotationsView(
+ private val sources: List,
+ private val includeInherited: Boolean = false,
+ ) : AnnotationSet {
+
+ private fun mirrors(source: Element): List {
+ if (!includeInherited) {
+ return source.annotationMirrors
+ }
+
+ val superclass = (source as? TypeElement)?.superclass
+ val inherited =
+ generateSequence(superclass) { mirror -> (types.asElement(mirror) as? TypeElement)?.superclass }
+ .mapNotNull { types.asElement(it) as? TypeElement }
+ .takeWhile { it.qualifiedName.toString() != Any::class.java.name }
+ .flatMap { supertype ->
+ supertype
+ .annotationMirrors
+ .filter { it.annotationType.asElement().hasAnnotation(Inherited::class.java) }
+ }
+ .toList()
+
+ return source.annotationMirrors + inherited
+ }
+
+ override fun contains(simpleName: String): Boolean =
+ sources.any { source ->
+ mirrors(source).any {
+ it.annotationType.asElement().simpleName.contentEquals(simpleName)
+ }
+ }
+
+ override fun find(type: Class): AnnotationProjection? =
+ sources
+ .firstNotNullOfOrNull { source -> mirrors(source).firstOrNull { it.named(type) } }
+ ?.let { JapAnnotationProjection(it) }
+
+ override fun findAll(type: Class): List {
+ val annotationMirrors = sources.flatMap { mirrors(it) }
+ val direct = annotationMirrors.filter { it.named(type) }.map { JapAnnotationProjection(it) }
+ // javac wraps repeated annotations in their @Repeatable container; unwrap its `value` array
+ val containerName = type.getAnnotation(JavaRepeatable::class.java)?.value?.java?.canonicalName
+ val container = containerName?.let { name -> annotationMirrors.firstOrNull { it.named(name) } }
+ val repeated = (container?.let { mirrorValues(it)["value"] } as? List<*>)
+ ?.filterIsInstance