javalin / javalin-openapi

Annotation processor for compile-time OpenAPI & JsonSchema, with out-of-the-box support for Javalin 5.x, Swagger & ReDoc
https://github.com/javalin/javalin-openapi/wiki
Apache License 2.0
45 stars 17 forks source link

Any patterns for keeping annotation values and javalin endpoint declarations in sync? #228

Closed kfp closed 2 weeks ago

kfp commented 2 weeks ago

I'm worried about making changes to a configured route and forgetting or otherwise botching the update to the annotation. Are there particular ways to help make sure that the values in the OpenAPI annotations (things like path, methods, return type) are in sync with the actual configured routes? I can think of two ways:

I'm curious if there are examples of doing this or other ways that have been used effectively.

dzikoysk commented 2 weeks ago

Hey, that's completely valid question and quite common issue for any kind of documentation/specification.

By design, it's unfortunately hard to achieve with annotation-based model that we're supporting - these annotations can be placed basically everywhere, because we're not limiting their scope. To be fair, you can even use this annotation processor without Javalin and it'll still allow you to describe the schema.

Considering some other approaches, it might be a little bit better, but in real-world scenarios you'll still end up in pretty much the same place, because the automation could cover only the most basic stuff we can get from the signature/handler metadata, but nothing more than that (like what's actually going on in the handler implementation).

My recommendation for now is to keep your endpoint handler and OpenApi in the same place, so everything you do on the http response is clearly visible within the same function and you don't need a deeper knowledge on what's going on further in your system. A quick example:

class UserEndpoints(private val userService: UserService) {
    @OpenApi(
        path = "/api/user/{id}"
        methods = { GET },
        summary = "Find user by id",
        pathParams = { @OpenApiParam(name = "id", description = "User id") },
        responses = { 
            @OpenApiResponse(status = "401", description = "Invalid input", content = @OpenApiContent(from = ErrorDto.class))
            @OpenApiResponse(status = "404", description = "User not found", content = @OpenApiContent(from = ErrorDto.class))
            @OpenApiResponse(status = "200", description = "User data", content = @OpenApiContent(from = User.class)) 
        },
    )
    fun findUserById(ctx: Context) {
        val userId = ctx.pathParam("id") ?: throw BadRequestResponse() // you'll usually do some input validation that may result in different responses/http codes
        val user = userService.getUser(userId) ?: throw NotFoundResponse() // you should delegate your logic to standalone services and `Context` instance should absolutely never leave the scope of this endpoint function
        ctx.json(user) // convert service response to http response
    }
}

class UserService {
  fun getUser(id: String): User? = // typical domain implementation, should not touch http at all
}

As we can already see, there's no way we can predict what will be the response of this HTTP endpoint based on function signature - it's up to the input values that will be provided at runtime. This approach does not handle docs maintaince for us automatically, but at least helps to show what's going on as explicit as possible.

kfp commented 2 weeks ago

Hey thanks for the quick response! That all makes sense.

I came across this lib I'm going to investigate which seems promising form a testing perspective: https://www.hascode.com/2018/08/testing-openapi-swagger-schema-compliance-with-java-junit-and-assertj-swagger/

dzikoysk commented 2 weeks ago

Sure, if you'll find this library useful, feel free to share some feedback here too :) In case you'd be interested, we also have a tutorial section for community-driven posts:

I'll close the ticket, because I think that's pretty much all we've got right now. We also have some ongoing initiatives related to that, like e.g. #49, but it's already tracked out there :)