I'm Tristan and I'm working as a IT Security & Site Reliability Lead @sipgate.
- π¬ Ask me about IaC and DevOps in general
- π± Iβm currently learning rust
- π Pronouns: he/him
- π« How to reach me: π¦ @derveloper
Build your OpenApi3 spec in kotlin!
License: Apache License 2.0
I'm Tristan and I'm working as a IT Security & Site Reliability Lead @sipgate.
For a data class defined as follows,
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Schema
// ...
@Schema(description = "Test DateWrapper Description")
data class DateWrapper(
@Parameter(description = "test description")
val startDate: LocalDate,
val endDate: LocalDate? = null,
)
the output of the json is as follows.
"DateWrapper": {
"description": "Test DateWrapper Description",
"type": "object",
"properties": {
"endDate": {
"format": "date",
"type": "string"
},
"startDate": {
"format": "date",
"type": "string"
},
}
},
I am trying to provide a description for the Property (startDate
) in the Schema. Could you provide some guidance on how to do achieve this? I am looking for an output as follows:
"DateWrapper": {
"description": "Test DateWrapper Description",
"type": "object",
"properties": {
"endDate": {
"format": "date",
"type": "string"
},
"startDate": {
"format": "date",
"type": "string",
"description" : "test description"
},
}
},
Currently the "ByteArray" is not supported by the "findSchema" function.
I propose the add the following mapping
when (clazz) {
// ...
ByteArray::class-> StringSchema().format("binary")
// ...
}
This would be compliant with the documentation.
I could create a PR for this if the solution is accepted.
I am currently using the following to define required properties from Kotlin data classes:
data class CarView(
@field:Schema(required = true) val label: String,
val description: String?,
)
It should be great if we can avoid all this boilerplate by expressing the requirement with Kotlin Null Safety. So in previous code we do not need to annotate every field.
I think it could be expressed in the function findSchema
The inline functions have the disadvantage that they can just be called from other inline functions. If you have class as a variable (for example from a java interopt) you currently cannot use a lot of the DSL.
Example:
fun myFunction(kClass: KClass<*>) {
findSchema<kClass>(); // This is not possible
}
For every inline function create an alternative which gets the class as a value.
As an example:
// Current
inline fun <reified T> findSchema(): Schema<*>? {
return getEnumSchema<T>() ?: when (T::class) {
String::class -> StringSchema()
Boolean::class -> BooleanSchema()
java.lang.Boolean::class -> BooleanSchema()
Int::class -> IntegerSchema()
Integer::class -> IntegerSchema()
List::class -> ArraySchema()
Long::class -> IntegerSchema().format("int64")
BigDecimal::class -> IntegerSchema().format("")
Date::class -> DateSchema()
LocalDate::class -> DateSchema()
LocalDateTime::class -> DateTimeSchema()
else -> ModelConverters.getInstance().read(T::class.java)[T::class.java.simpleName]
}
}
// Proposal
inline fun <reified T> findSchema(): Schema<*>? = findSchema(T::class)
fun findSchema(kClass: KClass<*>): Schema<*>? {
return getEnumSchema(kClass) ?: when (kClass) {
String::class -> StringSchema()
Boolean::class -> BooleanSchema()
java.lang.Boolean::class -> BooleanSchema()
Int::class -> IntegerSchema()
Integer::class -> IntegerSchema()
List::class -> ArraySchema()
Long::class -> IntegerSchema().format("int64")
BigDecimal::class -> IntegerSchema().format("")
Date::class -> DateSchema()
LocalDate::class -> DateSchema()
LocalDateTime::class -> DateTimeSchema()
else -> ModelConverters.getInstance().read(kClass.java)[kClass.java.simpleName]
}
}
If you accept the proposal I could create a PR for this.
The existing Components.securityScheme method registers the scheme using SecurityScheme.type. It think it might be better to use a construct like:
fun Components.securityScheme(name: String, init: SecurityScheme.() -> Unit) {
val security = SecurityScheme()
security.init()
securitySchemes = securitySchemes ?: mutableMapOf()
securitySchemes[name] = security
}
and invoke it like:
securityScheme("bearerAuth") {
type = SecurityScheme.Type.HTTP
scheme = "bearer"
bearerFormat = "JWT"
}
securityScheme("apiKeyAuth") {
type = SecurityScheme.Type.APIKEY
`in` = SecurityScheme.In.HEADER
name = "X-API-Key"
}
This would produce json that is valid according to https://editor.swagger.io/:
"securitySchemes": {
"apiKeyAuth": {
"in": "header",
"name": "X-API-Key",
"type": "apiKey"
},
"bearerAuth": {
"bearerFormat": "JWT",
"scheme": "bearer",
"type": "http"
}
}
With regard to a comment in the code base, I donΒ΄t think it is correct to use SecurityScheme.name since it has another meaning.
it would be nice to be able to define all reusable components (parameters, responses ..) in the components section
and make it possible to reference them ... maybe like parameterRef("myParam")
would that be a useful feature?
This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.
This repository currently has no open or pending branches.
.github/workflows/gradle.yml
actions/checkout v4@a5ac7e51b41094c92402da3b24376905380afc29
actions/setup-java v4
.github/workflows/publish.yml
actions/checkout v4@a5ac7e51b41094c92402da3b24376905380afc29
actions/setup-java v4
settings.gradle
build.gradle
org.jetbrains.kotlin:kotlin-gradle-plugin 2.0.0
io.github.gradle-nexus.publish-plugin 2.0.0
org.jetbrains.kotlin:kotlin-stdlib 2.0.0
com.fasterxml.jackson.core:jackson-core 2.17.1
com.fasterxml.jackson.core:jackson-databind 2.17.1
com.fasterxml.jackson.module:jackson-module-jsonSchema 2.17.1
com.fasterxml.jackson.datatype:jackson-datatype-json-org 2.17.1
io.swagger.core.v3:swagger-core 2.2.22
io.swagger.parser.v3:swagger-parser 2.1.22
org.jetbrains.kotlin:kotlin-reflect 2.0.0
io.kotlintest:kotlintest-runner-junit5 3.4.2
org.slf4j:slf4j-simple 2.0.13
gradle/wrapper/gradle-wrapper.properties
gradle 8.8
With this updated version, my application is no longer compiling the swagger classes because they are being used at runtime now. I see in your gradle file that you changed them from 'compile' to 'implementation'. In my maven dependency tree it used to be 'io.swagger.core.v3:swagger-core...:compile'. Now it says 'io.swagger.core.v3:swagger-core...:runtime. Would you be wiling to change libraries back to compile scope instead of runtime scope. If not then you can go ahead and close this issue.
I tried to use kotlin-openapi3-dsl plugin with Javalin according to this documentation: https://javalin.io/plugins/openapi
In my sample class here: https://github.com/pwittchen/money-transfer-api/blob/master/src/main/java/com/pwittchen/money/transfer/api/controller/AccountController.java
I added annotations as follows:
public class AccountController {
private ContextWrapper contextWrapper;
private AccountRepository accountRepository;
@Inject
public AccountController(
final ContextWrapper contextWrapper,
final AccountRepository accountRepository) {
this.contextWrapper = contextWrapper;
this.accountRepository = accountRepository;
}
@OpenApi(
method = HttpMethod.GET,
path = "/account",
pathParams = @OpenApiParam(name = "id", type = Integer.class),
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Account.class))
)
public void getOne(Context context) {
Optional<Account> account = accountRepository.get(contextWrapper.pathParam(context, "id"));
if (account.isPresent()) {
contextWrapper.json(context, account.get());
} else {
String message = String.format(
"account with id %s does not exist",
contextWrapper.pathParam(context, "id")
);
Response response = Response.builder()
.message(message)
.build();
contextWrapper.json(context, response, 404);
}
}
@OpenApi(
method = HttpMethod.GET,
path = "/account",
responses = @OpenApiResponse(
status = "200",
content = @OpenApiContent(from = Account.class, isArray = true)
)
)
public void getAll(final Context context) {
contextWrapper.json(context, accountRepository.get());
}
@OpenApi(
method = HttpMethod.POST,
path = "/account",
pathParams = {
@OpenApiParam(name = "name"),
@OpenApiParam(name = "surname"),
@OpenApiParam(name = "currency"),
@OpenApiParam(name = "money")
},
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Response.class))
)
public void create(final Context context) {
User user = createUser(context);
Optional<Account> account = createAccount(context, user);
if (!account.isPresent()) {
Response response = Response.builder().message("Invalid money format").build();
contextWrapper.json(context, response);
return;
}
try {
accountRepository.create(account.get());
Response response = Response.builder()
.message("account created")
.object(account.get())
.build();
contextWrapper.json(context, response);
} catch (Exception exception) {
Response response = Response.builder().message(exception.getMessage()).build();
contextWrapper.json(context, response);
}
}
private User createUser(Context context) {
return User.builder()
.id(UUID.randomUUID().toString())
.name(contextWrapper.formParam(context, "name"))
.surname(contextWrapper.formParam(context, "surname"))
.build();
}
private Optional<Account> createAccount(Context context, User user) {
return parseMoney(context)
.map(money -> Account.builder()
.number(UUID.randomUUID().toString())
.user(user)
.money(money)
.createdAt(LocalDateTime.now())
.build()
);
}
private Optional<Money> parseMoney(Context context) {
try {
return Optional.of(Money.parse(String.format("%s %s",
contextWrapper.formParam(context, "currency"),
contextWrapper.formParam(context, "money"))
));
} catch (Exception exception) {
return Optional.empty();
}
}
@OpenApi(
method = HttpMethod.DELETE,
path = "/account",
pathParams = @OpenApiParam(name = "id", type = Integer.class),
responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Response.class))
)
public void delete(Context context) {
try {
accountRepository.delete(contextWrapper.pathParam(context, "id"));
String message = String.format(
"account with number %s deleted",
contextWrapper.pathParam(context, "id")
);
Response response = Response.builder()
.message(message)
.build();
contextWrapper.json(context, response);
} catch (Exception exception) {
Response response = Response.builder().message(exception.getMessage()).build();
contextWrapper.json(context, response);
}
}
and I registered plugin:
config.registerPlugin(new OpenApiPlugin(
new OpenApiOptions(new Info()
.version("1.0")
.description("Money Transfer API"))
.path("/openapi")
.activateAnnotationScanningFor(
"com.github.pwittchen.money.transfer.api.controller"
)
)
);
Then, when I open ReDoc, I see the following errors in the console:
[main] INFO com.pwittchen.money.transfer.api.Application - server has started
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
[qtp728885526-17] INFO com.pwittchen.money.transfer.api.Application - 463.79935 ms GET /openapi
It says, I should add path
and method
to the annotations, but I already did it!
I did everything what documentation says, but I didn't get expected result. I tried to use this without annotations, but it didn't work either.
Documentation with ReDoc works in general, but I'm not able to document exact response values and types and other details.
I think your DSL could benefit greatly from using a DSLMarker annotation.
Consider defining an annotation like:
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@DslMarker
annotation class OpenApiDsl
Then annotate the API receiver lambdas like:
fun OpenAPI.paths(init: @OpenApiDsl Paths.() -> Unit) {
paths = paths ?: Paths()
paths.init()
}
Sadly, this could potentially break existing code.
I have found an issue while creating an OpenAPI definition with this library.
I define my operation responses as:
responses {
response("204") {
description = "..."
}
responses.putAll(standardResponses)
}
Where standardResponses is a collection of common responses. The responses
method is a builder for ApiResponses
class from swagger that implement LinkedHashMap
so the order should be preserved.
Everything should work but the output does not keep the same order I used while inserting the responses, the cuprit seems to be the JSONObject
used for formatting:
Looks like there is a debate about this problem at the moment, see issue
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.