From 8013325fbe99bb229a862b8ca940f56b9f4904b3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 4 Oct 2025 22:40:56 +0300 Subject: [PATCH] fix: update javalin-maven-java example --- examples/javalin-maven-java/pom.xml | 37 + .../openapi/plugin/test/JavalinTest.java | 640 ++++++++++++------ 2 files changed, 483 insertions(+), 194 deletions(-) diff --git a/examples/javalin-maven-java/pom.xml b/examples/javalin-maven-java/pom.xml index 5a6049e..e27db83 100644 --- a/examples/javalin-maven-java/pom.xml +++ b/examples/javalin-maven-java/pom.xml @@ -23,11 +23,14 @@ + io.javalin javalin ${javalin.version} + + io.javalin.community.openapi javalin-openapi-plugin @@ -43,6 +46,8 @@ javalin-redoc-plugin ${javalin.openapi.version} + + org.webjars.npm redoc @@ -54,6 +59,8 @@ + + org.tinylog tinylog-api @@ -69,6 +76,28 @@ slf4j-tinylog 2.4.1 + + + + org.projectlombok + lombok + 1.18.28 + provided + + + + + jakarta.validation + jakarta.validation-api + 2.0.2 + + + + + org.mongodb + bson + 4.9.1 + @@ -80,11 +109,19 @@ 3.10.1 + io.javalin.community.openapi openapi-annotation-processor ${javalin.openapi.version} + + + + org.projectlombok + lombok + 1.18.28 + 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 4333929..1abdd22 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 @@ -4,207 +4,239 @@ import io.javalin.Javalin; import io.javalin.http.Context; import io.javalin.http.Handler; -import io.javalin.openapi.ApiKeyAuth; -import io.javalin.openapi.BasicAuth; -import io.javalin.openapi.BearerAuth; -import io.javalin.openapi.CookieAuth; -import io.javalin.openapi.HttpMethod; -import io.javalin.openapi.ImplicitFlow; -import io.javalin.openapi.OAuth2; -import io.javalin.openapi.OpenApi; -import io.javalin.openapi.OpenApiContact; -import io.javalin.openapi.OpenApiContent; -import io.javalin.openapi.OpenApiContentProperty; -import io.javalin.openapi.OpenApiExample; -import io.javalin.openapi.OpenApiIgnore; -import io.javalin.openapi.OpenApiInfo; -import io.javalin.openapi.OpenApiLicense; -import io.javalin.openapi.OpenApiName; -import io.javalin.openapi.OpenApiParam; -import io.javalin.openapi.OpenApiPropertyType; -import io.javalin.openapi.OpenApiRequestBody; -import io.javalin.openapi.OpenApiResponse; -import io.javalin.openapi.OpenApiSecurity; -import io.javalin.openapi.OpenApiServer; -import io.javalin.openapi.OpenApiServerVariable; -import io.javalin.openapi.OpenID; -import io.javalin.openapi.Security; -import io.javalin.openapi.plugin.OpenApiConfiguration; +import io.javalin.openapi.*; import io.javalin.openapi.plugin.OpenApiPlugin; -import io.javalin.openapi.plugin.SecurityConfiguration; -import io.javalin.openapi.plugin.redoc.ReDocConfiguration; import io.javalin.openapi.plugin.redoc.ReDocPlugin; -import io.javalin.openapi.plugin.swagger.SwaggerConfiguration; import io.javalin.openapi.plugin.swagger.SwaggerPlugin; +import io.javalin.security.RouteRole; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.bson.types.ObjectId; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.File; import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; -import static java.util.Map.entry; +import javax.validation.constraints.NotEmpty; /** * Starts Javalin server with OpenAPI plugin */ public final class JavalinTest implements Handler { + enum Rules implements RouteRole { + ANONYMOUS, + USER, + } + /** * Runs server on localhost:8080 * * @param args args */ public static void main(String[] args) { - Javalin.create(config -> { - String deprecatedDocsPath = "/swagger-docs"; - - OpenApiContact openApiContact = new OpenApiContact(); - openApiContact.setName("API Support"); - openApiContact.setUrl("https://www.example.com/support"); - openApiContact.setEmail("support@example.com"); - - OpenApiLicense openApiLicense = new OpenApiLicense(); - openApiLicense.setName("Apache 2.0"); - openApiLicense.setIdentifier("Apache-2.0"); - - OpenApiInfo openApiInfo = new OpenApiInfo(); - openApiInfo.setTitle("Awesome App"); - openApiInfo.setSummary("App summary"); - openApiInfo.setDescription("App description goes right here"); - openApiInfo.setTermsOfService("https://example.com/tos"); - openApiInfo.setContact(openApiContact); - openApiInfo.setLicense(openApiLicense); - openApiInfo.setVersion("1.0.0"); - - OpenApiServerVariable portServerVariable = new OpenApiServerVariable(); - portServerVariable.setValues(new String[] { "7070", "8080" }); - portServerVariable.setDefault("8080"); - portServerVariable.setDescription("Port of the server"); - - OpenApiServerVariable basePathServerVariable = new OpenApiServerVariable(); - basePathServerVariable.setValues(new String[] { "v1" }); - basePathServerVariable.setDefault("v1"); - basePathServerVariable.setDescription("Base path of the server"); - - OpenApiServer openApiServer = new OpenApiServer(); - openApiServer.setUrl("https://example.com:{port}/{basePath}"); - openApiServer.setDescription("Server description goes here"); - openApiServer.addVariable("port", portServerVariable); - openApiServer.addVariable("basePath", basePathServerVariable); - - OpenApiServer[] servers = new OpenApiServer[] { openApiServer }; - - OpenApiConfiguration openApiConfiguration = new OpenApiConfiguration(); - openApiConfiguration.setInfo(openApiInfo); - openApiConfiguration.setServers(servers); - openApiConfiguration.setDocumentationPath(deprecatedDocsPath); // by default it's /openapi - // Based on official example: https://swagger.io/docs/specification/authentication/oauth2/ - openApiConfiguration.setSecurity(new SecurityConfiguration( - Map.ofEntries( - entry("BasicAuth", new BasicAuth()), - entry("BearerAuth", new BearerAuth()), - entry("ApiKeyAuth", new ApiKeyAuth()), - entry("CookieAuth", new CookieAuth("JSESSIONID")), - entry("OpenID", new OpenID("https://example.com/.well-known/openid-configuration")), - entry("OAuth2", new OAuth2( - "This API uses OAuth 2 with the implicit grant flow.", - List.of( - new ImplicitFlow( - "https://api.example.com/oauth2/authorize", - new HashMap<>() {{ - put("read_pets", "read your pets"); - put("write_pets", "modify pets in your account"); - }} - ) - ) - )) - ), - List.of( - new Security( - "oauth2", - List.of( - "write_pets", - "read_pets" - ) - ) - ) - )); - openApiConfiguration.setDocumentProcessor(docs -> { // you can add whatever you want to this document using your favourite json api - docs.set("test", new TextNode("Value")); - return docs.toPrettyString(); - }); - config.plugins.register(new OpenApiPlugin(openApiConfiguration)); - - SwaggerConfiguration swaggerConfiguration = new SwaggerConfiguration(); - swaggerConfiguration.setDocumentationPath(deprecatedDocsPath); - config.plugins.register(new SwaggerPlugin(swaggerConfiguration)); - - ReDocConfiguration reDocConfiguration = new ReDocConfiguration(); - reDocConfiguration.setDocumentationPath(deprecatedDocsPath); - config.plugins.register(new ReDocPlugin(reDocConfiguration)); - }) - .start(8080); + Javalin.createAndStart(config -> { + // config.routing.contextPath = "/custom"; + String deprecatedDocsPath = "/api/openapi.json"; // by default it's /openapi + + config.registerPlugin(new OpenApiPlugin(openApiConfig -> + openApiConfig + .withDocumentationPath(deprecatedDocsPath) + .withRoles(Rules.ANONYMOUS) + .withPrettyOutput() + .withDefinitionConfiguration((version, openApiDefinition) -> + openApiDefinition + .withInfo(openApiInfo -> + openApiInfo + .description("App description goes right here") + .termsOfService("https://example.com/tos") + .contact("API Support", "https://www.example.com/support", "support@example.com") + .license("Apache 2.0", "https://www.apache.org/licenses/", "Apache-2.0") + ) + .withServer(openApiServer -> + openApiServer + .description("Server description goes here") + .url("http://localhost:{port}{basePath}/" + version + "/") + .variable("port", "Server's port", "8080", "8080", "7070") + .variable("basePath", "Base path of the server", "", "", "/v1") + ) + // Based on official example: https://swagger.io/docs/specification/authentication/oauth2/ + .withSecurity(openApiSecurity -> + openApiSecurity + .withBasicAuth() + .withBearerAuth() + .withApiKeyAuth("ApiKeyAuth", "X-Api-Key") + .withCookieAuth("CookieAuth", "JSESSIONID") + .withOpenID("OpenID", "https://example.com/.well-known/openid-configuration") + .withOAuth2("OAuth2", "This API uses OAuth 2 with the implicit grant flow.", oauth2 -> + oauth2 + .withClientCredentials("https://api.example.com/credentials/authorize") + .withImplicitFlow("https://api.example.com/oauth2/authorize", flow -> + flow + .withScope("read_pets", "read your pets") + .withScope("write_pets", "modify pets in your account") + ) + ) + .withGlobalSecurity("OAuth2", globalSecurity -> + globalSecurity + .withScope("write_pets") + .withScope("read_pets") + ) + .withGlobalSecurity("BearerAuth") + ) + .withDefinitionProcessor(content -> { // you can add whatever you want to this document using your favourite json api + content.set("test", new TextNode("Value")); + return content.toPrettyString(); + }) + ))); + + config.registerPlugin(new SwaggerPlugin(swaggerConfiguration -> swaggerConfiguration.setDocumentationPath(deprecatedDocsPath))); + + config.registerPlugin(new ReDocPlugin(reDocConfiguration -> reDocConfiguration.setDocumentationPath(deprecatedDocsPath))); + + for (JsonSchemaResource generatedJsonSchema : new JsonSchemaLoader().loadGeneratedSchemes()) { + System.out.println(generatedJsonSchema.getName()); + System.out.println(generatedJsonSchema.getContentAsString()); + } + }); } - private static final String ROUTE = "/main/{name}"; - - @Override @OpenApi( - path = ROUTE, - operationId = "cli", - methods = HttpMethod.POST, - summary = "Remote command execution", - description = "Execute command using POST request. The commands are the same as in the console and can be listed using the 'help' command.", - tags = { "Cli" }, - security = { - @OpenApiSecurity(name = "BasicAuth") - }, - requestBody = @OpenApiRequestBody( - content = { - @OpenApiContent(from = String.class), // simple type - @OpenApiContent(from = EntityDto[].class), // array - @OpenApiContent(from = LombokEntity.class), // lombok - @OpenApiContent(from = KotlinEntity.class), // kotlin - @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 - @OpenApiContentProperty(name = "reference", from = KotlinEntity.class) // reference to another object - @OpenApiContentProperty(name = "file-name", isArray = true, type = "string", format = "base64") // multi-file upload - }) - } - ), - headers = { - //@OpenApiParam(name = "Authorization", description = "Alias and token provided as basic auth credentials", required = true, type = UUID.class), - @OpenApiParam(name = "Optional"), - @OpenApiParam(name = "X-Rick", example = "Rolled"), - @OpenApiParam(name = "X-SomeNumber", required = true, type = Integer.class, example = "500") - }, - pathParams = { - @OpenApiParam(name = "name", description = "Name", required = true, type = UUID.class) - }, - responses = { - @OpenApiResponse(status = "200", description = "Status of the executed command", content = { - @OpenApiContent(from = EntityDto[].class) - }), - @OpenApiResponse( - status = "400", - description = "Error message related to the invalid command format (0 < command length < " + 10 + ")", - content = @OpenApiContent(from = EntityDto[].class) + path = "/main/{name}", + methods = HttpMethod.POST, + operationId = "cli", + summary = "Remote command execution", + description = "Execute command using POST request. The commands are the same as in the console and can be listed using the 'help' command.", + tags = { "Default", "Cli" }, + security = { + @OpenApiSecurity(name = "BasicAuth") + }, + headers = { + @OpenApiParam(name = "Authorization", description = "Alias and token provided as basic auth credentials", required = true, type = UUID.class), + @OpenApiParam(name = "Optional"), + @OpenApiParam(name = "X-Rick", example = "Rolled"), + @OpenApiParam(name = "X-SomeNumber", required = true, type = Integer.class, example = "500") + }, + pathParams = { + @OpenApiParam(name = "name", description = "Name", required = true, type = UUID.class) + }, + queryParams = { + @OpenApiParam(name = "query", description = "Some query", required = true, type = Integer.class) + }, + requestBody = @OpenApiRequestBody( + description = "Supports multiple request bodies", + content = { + @OpenApiContent(from = String.class, example = "value"), // simple type + @OpenApiContent(from = String[].class, example = "value"), // array of simple types + @OpenApiContent( // map of simple types + mimeType = "application/map-string-string", + additionalProperties = @OpenApiAdditionalContent( + from = String.class, + exampleObjects = { + @OpenApiExampleProperty(name = "en", value = "hey"), + @OpenApiExampleProperty(name = "pl", value = "hejka tu lenka"), + } + ) + ), + @OpenApiContent( // map of complex types + mimeType = "application/map-string-object", + additionalProperties = @OpenApiAdditionalContent(from = Foo.class) + ), + @OpenApiContent(from = KotlinEntity.class, mimeType = "app/barbie", exampleObjects = { + @OpenApiExampleProperty(name = "name", value = "Margot Robbie") + }), // kotlin + @OpenApiContent(from = LombokEntity.class, mimeType = "app/lombok"), // lombok + @OpenApiContent(from = EntityWithGenericType.class), // generics + @OpenApiContent(from = RecordEntity.class, mimeType = "app/record"), // record class + @OpenApiContent(from = DtoWithFields.class, mimeType = "app/dto-fields"), // map only fields + @OpenApiContent(from = DtoWithFieldsAndMethods.class, mimeType = "app/dto-fields-and-methods"), // map fields and methods + @OpenApiContent(from = EnumEntity.class, mimeType = "app/enum"), // enum, + @OpenApiContent(from = CustomNameEntity.class, mimeType = "app/custom-name-entity") // custom name + } ), - @OpenApiResponse(status = "401", description = "Error message related to the unauthorized access", content = { - @OpenApiContent(from = EntityDto[].class) - }) - } + responses = { + @OpenApiResponse(status = "200", description = "Status of the executed command", content = { + @OpenApiContent(from = String.class, example = "Value"), + @OpenApiContent(from = EntityDto[].class) + }), + @OpenApiResponse( + status = "400", + description = "Error message related to the invalid command format (0 < command length < " + 10 + ")", + content = @OpenApiContent(from = EntityDto[].class), + headers = { + @OpenApiParam(name = "X-Error-Message", description = "Error message") + } + ), + @OpenApiResponse(status = "401", description = "Error message related to the unauthorized access", content = { + @OpenApiContent(from = EntityDto[].class, exampleObjects = { + @OpenApiExampleProperty(name = "error", value = "ERROR-CODE-401"), + }) + }), + @OpenApiResponse(status = "500") // fill description with HttpStatus message + }, + callbacks = { + @OpenApiCallback( + name = "onData", + url = "{$request.query.callbackUrl}/data", + method = HttpMethod.GET, + requestBody = @OpenApiRequestBody( + description = "Callback request body", + content = @OpenApiContent(from = String.class) + ), + responses = { + @OpenApiResponse( + status = "200", + description = "Callback response", + content = { @OpenApiContent(from = String.class) } + ) + } + ), + } + ) + @OpenApi( + path = "/repeatable", + methods = { HttpMethod.POST }, + versions = "v1", + requestBody = @OpenApiRequestBody( + description = "Complex bodies", + content = { + @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 + @OpenApiContentProperty(name = "reference", from = KotlinEntity.class), // reference to another object + @OpenApiContentProperty(name = "file-name", isArray = true, type = "string", format = "base64") // multi-file upload + }) + } + ) ) - public void handle(@NotNull Context ctx) { } + @Override + public void handle(@NotNull Context ctx) {} + @OpenApi( + path = "/standalone", + methods = HttpMethod.DELETE, + versions = "v2", + headers = { @OpenApiParam(name = "V2") } + ) + @OpenApi( + path = "standalone", + methods = HttpMethod.DELETE, + versions = "v1", + headers = { @OpenApiParam(name = "V1") } + ) static final class EntityDto implements Serializable { private final int status; @@ -212,7 +244,12 @@ static final class EntityDto implements Serializable { private final @NotNull String timestamp; private final @NotNull Foo foo; private final @NotNull List foos; - + private final @NotNull Map> bars = new HashMap<>(); + private final @NotNull EnumEntity enumEntity = EnumEntity.TWO; + // should be displayed as standard json section + // should be ignored + @Getter + @Setter private Bar bar; public EntityDto(int status, @NotNull String message, @NotNull Foo foo, @NotNull List foos, @Nullable Bar bar) { @@ -224,14 +261,9 @@ public EntityDto(int status, @NotNull String message, @NotNull Foo foo, @NotNull this.timestamp = ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT); } - // should ignore - public void setBar(Bar bar) { - this.bar = bar; - } - - // should be displayed as standard json section - public Bar getBar() { - return bar; + // should be represented as object + public @NotNull Map> getBars() { + return bars; } // should be represented by array @@ -239,12 +271,28 @@ public Bar getBar() { return foos; } + // should handle fallback to object type + @SuppressWarnings("rawtypes") + public List getUnknowns() { + return foos; + } + + // should display enum + public @NotNull EnumEntity getEnumEntity() { + return enumEntity; + } + // should be displayed as string @OpenApiPropertyType(definedBy = String.class) public @NotNull Foo getFoo() { return foo; } + // HiddenEntity with @OpenApiPropertyType should be displayed as string + public HiddenEntity getHiddenEntity() { + return new HiddenEntity(); + } + // should support primitive types public int getStatus() { return status; @@ -256,7 +304,7 @@ public String getMessageValue() { return message; } - // should ignore + // should be ignored @OpenApiIgnore public String getFormattedMessage() { return status + message; @@ -268,43 +316,247 @@ public String getFormattedMessage() { return timestamp; } + // should contain examples + @OpenApiExample(objects = { + @OpenApiExampleProperty(value = "2022-08-14T21:13:03.546Z"), + @OpenApiExampleProperty(value = "2022-08-14T21:13:03.546Z") + }) + public @NotNull String[] getTimestamps() { + return new String[] { timestamp }; + } + + // should contain dedicated foo example + @OpenApiExample(objects = { + @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), + @OpenApiExampleProperty(name = "link", value = "Dedicated link") + }) + public @NotNull Foo getExampleFoo() { + return new Foo(); + } + + // should contain object example + @OpenApiExample(objects = { + @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), + @OpenApiExampleProperty(name = "link", value = "https://www.youtube.com/watch?v=dQw4w9WgXcQ") + }) + public @NotNull Object getExampleObject() { + return new String[] { timestamp }; + } + + // should contain objects example + @OpenApiExample(objects = { + @OpenApiExampleProperty(name = "Barbie", objects = { + @OpenApiExampleProperty(name = "name", value = "Margot Robbie"), + @OpenApiExampleProperty(name = "link", value = "https://www.youtube.com/watch?v=dQw4w9WgXcQ") + }), + }) + public @NotNull Object[] getExampleObjects() { + return new String[] { timestamp }; + } + // should contain example for primitive types, SwaggerUI will automatically display this as an Integer @OpenApiExample("5050") + @OpenApiNumberValidation( + minimum = "5000", + exclusiveMinimum = true, + maximum = "6000", + exclusiveMaximum = true, + multipleOf = "50" + ) + @OpenApiStringValidation( + minLength = "4", + maxLength = "4", + pattern = "^[0-9]{4}$", + format = "int32" + ) + @OpenApiArrayValidation( + minItems = "1", + maxItems = "1", + uniqueItems = true + ) + @OpenApiObjectValidation( + minProperties = "1", + maxProperties = "1" + ) public int getVeryImportantNumber() { return status + 1; } - } + @OpenApiDescription("Some description") + public String getDescription() { + return "Description"; + } - static final class Foo { + // should support @Custom from JsonSchema + @Custom(name = "description", value = "Custom property") + public String getCustom() { + return ""; + } - private String property; - private String link; + // should be displayed as string + public ObjectId getObjectId() { + return new ObjectId(); + } - public String getProperty() { - return property; + // static should be ignored + public static String getStatic() { + return "static"; } + // by default nullable fields are not required, but we can force it + //@OpenApiRequired + public JsonSchemaEntity getNullableIsRequired() { + return null; + } + + } + + static final class Foo { + @OpenApiExample("https://www.youtube.com/watch?v=dQw4w9WgXcQ") public String getLink() { - return link; + return ""; } - } + // subtype + @Getter static final class Bar { + private String property; + } + + // should work with properties generated by another annotation processor + @Data + static final class LombokEntity { private String property; + } + + // should pick upper/lower bound type for generics + @Getter + static final class EntityWithGenericType { - public String getProperty() { - return property; + private final V value; + + public EntityWithGenericType(V value) { + this.value = value; } } - @Data - static final class LombokEntity { - private String property; + // should query fields + @OpenApiByFields(value = Visibility.PROTECTED, only = true) // by default: PUBLIC + static final class DtoWithFields { + + public String publicName; + String defaultName; + protected String protectedName; + private String privateName; + + public String getCustom() { + return "custom"; + } + } + + // should query fields and methods + @OpenApiByFields(Visibility.PROTECTED) // by default: PUBLIC + static final class DtoWithFieldsAndMethods { + + public String publicName; + String defaultName; + protected String protectedName; + private String privateName; + + public String getCustom() { + return "custom"; + } + } + + enum EnumEntity { + + ONE("A"), + TWO("B"); + + private final String name; + + EnumEntity(String name) { + this.name = name; + } + + public String getName() { + Enum e = EnumEntity.TWO; + return name; + } + + } + + @JsonSchema(requireNonNulls = false) + static final class JsonSchemaEntity { + + @OpenApiRequired + public List getEntities() { + return Collections.emptyList(); + } + + @OneOf({ Panda.class, Cat.class }) + public Animal getAnimal() { + return new Panda(); + } + + } + + interface Animal { + + default boolean isAnimal() { + return true; + } + } + static class Panda implements Animal { + + @Custom(name = "title", value = "Panda") + @Custom(name = "description", value = "Only Panda") + public boolean isPanda() { + return true; + } + + } + + @Target({ ElementType.METHOD, ElementType.TYPE }) + @CustomAnnotation + @interface Description { + + String title(); + + String description(); + + int statusCode(); + } + + static class Cat implements Animal { + + @Description(title = "Cat", description = "Is it cat?", statusCode = 200) + public boolean isCat() { + return true; + } + + } + + @OpenApiPropertyType(definedBy = String.class) + static class HiddenEntity { + + } + + @OpenApiName("EntityWithCustomName") + static class CustomNameEntity {} + + @Data + @AllArgsConstructor + public static class RecordEntity { + @NotEmpty + private String name; + @NotEmpty + private String surname; + } }