Javalin: Problem with OpenAPI plugin (Java+Annotations)

Created on 14 Aug 2019  Â·  12Comments  Â·  Source: tipsy/javalin

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.

BUG

All 12 comments

Thanks for your thorough issue @pwittchen !

@TobiasWalle is the openapi expert, so I'm going to ping him yet again 👼

@pwittchen Thank you bringing up this issue

I was just able to reproduce it. I let you know once the fix ist ready.

Thanks for your time! I appreciate it. :-)

--
Piotr Wittchen,
http://wittchen.io

czw., 15 sie 2019, 12:10 użytkownik Tobias Walle notifications@github.com
napisał:

@pwittchen https://github.com/pwittchen Thank you bringing up this issue

I was just able to reproduce it. I let you know once the fix ist ready.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/tipsy/javalin/issues/709?email_source=notifications&email_token=AAFJYFZIMNOVUD25WBPX2PTQEUTSNA5CNFSM4ILY7I42YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4LNS3A#issuecomment-521591148,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAFJYF4A7ESJF7XIDLZ7G3TQEUTSNANCNFSM4ILY7I4Q
.

Okay @pwittchen I found the issue :)
The problem was that the given path was not correct. If you change the annotations of getOne and delete to the following, everything works as expected:

public class AccountController {
    @OpenApi(
        method = HttpMethod.GET,
        path = "/account/:id",
        pathParams = @OpenApiParam(name = "id", type = Integer.class),
        responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Account.class))
    )
    public void getOne(Context context) {
    ...
    }

   ...

    @OpenApi(
        method = HttpMethod.DELETE,
        path = "/account/:id",
        pathParams = @OpenApiParam(name = "id", type = Integer.class),
        responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Response.class))
    )
    public void delete(Context context) {
    ...
    }
}

But we can probably give some tipps in the error message, to make this error easier detectable in the future.

I created the PR which prints the following error message in your error case:

The `path` of one of the @OpenApi annotations on io.javalin.openapi.ClassHandlerWithInvalidPath is incorrect. The path param ":id" is documented, but couldn't be found in GET "/account". Do you mean GET "/account/:id"?

Thanks for finding source of the problem so quickly. I think improving error message will help a lot in diagnosing similar problems in the future.

It's also worth to mention in documentation or in the log messages/warnings that fields of the custom objects used by the Open API must be public (in my case Account class). Otherwise they won't be rendered in the json schema. I didn't know that, but just figured it out, because technically it makes sense :-).

Migrating from v2:

19:11:35.492 [DEBUG] [TestEventLogger]         Caused by:
19:11:35.492 [DEBUG] [TestEventLogger]         java.lang.reflect.InvocationTargetException
19:11:35.492 [DEBUG] [TestEventLogger]             at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
19:11:35.492 [DEBUG] [TestEventLogger]             at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
19:11:35.492 [DEBUG] [TestEventLogger]             at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
19:11:35.492 [DEBUG] [TestEventLogger]             at java.lang.reflect.Method.invoke(Method.java:498)
19:11:35.492 [DEBUG] [TestEventLogger]             at sparkles.support.javalin.testing.reflection.Reflection.invoke(Reflection.java:111)
19:11:35.492 [DEBUG] [TestEventLogger]             ... 35 more
19:11:35.492 [DEBUG] [TestEventLogger] 
19:11:35.492 [DEBUG] [TestEventLogger]             Caused by:
19:11:35.492 [DEBUG] [TestEventLogger]             java.lang.NoSuchMethodException: getAll
19:11:35.492 [DEBUG] [TestEventLogger]                 at io.javalin.plugin.openapi.dsl.OpenApiBuilder.moveDocumentationFromAnnotationToHandler(OpenApiBuilder.kt:62)
19:11:35.492 [DEBUG] [TestEventLogger]                 at io.javalin.plugin.openapi.dsl.OpenApiBuilder.moveDocumentationFromAnnotationToHandler(OpenApiBuilder.kt:53)
19:11:35.492 [DEBUG] [TestEventLogger]                 at io.javalin.plugin.openapi.dsl.OpenApiBuilder.documented(OpenApiBuilder.kt:44)
19:11:35.492 [DEBUG] [TestEventLogger]                 at io.javalin.apibuilder.ApiBuilder.crud(ApiBuilder.java:493)
19:11:35.492 [DEBUG] [TestEventLogger]                 at io.javalin.apibuilder.ApiBuilder.crud(ApiBuilder.java:472)
19:11:35.492 [DEBUG] [TestEventLogger]                 at sparkles.entity.FooApi.lambda$apply$2(FooApi.java:48)

Looks as if OpenApiBuilder requires that crud() handlers are documented. Is it intended?

https://github.com/tipsy/javalin/blob/2359c3ff88d626d40ae208fce5689b08befac043/src/main/java/io/javalin/apibuilder/ApiBuilder.java#L492-L493

@dherges No it's not. Thanks for pointing this out. I will fix the issue later.

@dherges The problem was that you probably used an extended class as a crud handler. As I found out, there were several issues with extended classes.

I created a PR which should fix your problems with the crud handler.
https://github.com/tipsy/javalin/pull/712

I think this can be closed

Thanks @TobiasWalle, I forgot about it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

bvicenzo picture bvicenzo  Â·  3Comments

maxemann96 picture maxemann96  Â·  5Comments

ShikaSD picture ShikaSD  Â·  5Comments

mkpaz picture mkpaz  Â·  4Comments

gane5h picture gane5h  Â·  3Comments