Open plblueraven opened 2 years ago
Did there is any news about this topic ?
for me i just activate hateoas on spring generaor plugin, but even input have the link that i dont want !, i want only for ouptut object
From my side i find a workaround by defining a custom "pojo.mustache
" template using Spring template as a base.
I added a new "vendorExtensions.x-custom-hateoas-link
" balise after the "vars
" ones:
/**
* {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}}
* @deprecated{{/isDeprecated}}
*/
{{>additionalModelTypeAnnotations}}
{{#description}}
{{#isDeprecated}}
@Deprecated
{{/isDeprecated}}
{{#swagger1AnnotationLibrary}}
@ApiModel(description = "{{{description}}}")
{{/swagger1AnnotationLibrary}}
{{#swagger2AnnotationLibrary}}
@Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}"{{#deprecated}}, deprecated = true{{/deprecated}})
{{/swagger2AnnotationLibrary}}
{{/description}}
{{#discriminator}}
{{>typeInfoAnnotation}}
{{/discriminator}}
{{#jackson}}
{{#isClassnameSanitized}}
{{^hasDiscriminatorWithNonEmptyMapping}}
@JsonTypeName("{{name}}")
{{/hasDiscriminatorWithNonEmptyMapping}}
{{/isClassnameSanitized}}
{{/jackson}}
{{#withXml}}
{{>xmlAnnotation}}
{{/withXml}}
{{>generatedAnnotation}}
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
{{#serializableModel}}
private static final long serialVersionUID = 1L;
{{/serializableModel}}
{{#vars}}
{{^vendorExtensions.x-custom-hateoas-link}}
{{#isEnum}}
{{^isContainer}}
{{>enumClass}}
{{/isContainer}}
{{#isContainer}}
{{#mostInnerItems}}
{{>enumClass}}
{{/mostInnerItems}}
{{/isContainer}}
{{/isEnum}}
{{#gson}}
@SerializedName("{{baseName}}")
{{/gson}}
{{#lombok.RequiredArgsConstructor}}
{{^useBeanValidation}}
{{#required}}
@lombok.NonNull
{{/required}}
{{/useBeanValidation}}
{{/lombok.RequiredArgsConstructor}}
{{#lombok.ToString}}
{{#isPassword}}
@lombok.ToString.Exclude
{{/isPassword}}
{{/lombok.ToString}}
{{#vendorExtensions.x-field-extra-annotation}}
{{{vendorExtensions.x-field-extra-annotation}}}
{{/vendorExtensions.x-field-extra-annotation}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
{{#isContainer}}
{{#useBeanValidation}}@Valid{{/useBeanValidation}}
{{#openApiNullable}}
private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}
{{/openApiNullable}}
{{^openApiNullable}}
private {{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
{{/openApiNullable}}
{{/isContainer}}
{{^isContainer}}
{{#isDate}}
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
{{/isDate}}
{{#isDateTime}}
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
{{/isDateTime}}
{{#openApiNullable}}
private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#useOptional}} = Optional.{{^defaultValue}}empty(){{/defaultValue}}{{#defaultValue}}of({{{.}}}){{/defaultValue}};{{/useOptional}}{{^useOptional}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/useOptional}}{{/isNullable}}{{/required}}{{^isNullable}}{{#required}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/required}}{{/isNullable}}
{{/openApiNullable}}
{{^openApiNullable}}
private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
{{/openApiNullable}}
{{/isContainer}}
{{/vendorExtensions.x-custom-hateoas-link}}
{{/vars}}
{{^lombok.Data}}
{{^lombok.RequiredArgsConstructor}}
{{#generatedConstructorWithRequiredArgs}}
{{#hasRequired}}
{{^lombok.NoArgsConstructor}}
public {{classname}}() {
super();
}
{{/lombok.NoArgsConstructor}}
/**
* Constructor with only required parameters
*/
public {{classname}}({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) {
{{#parent}}
super({{#parentRequiredVars}}{{name}}{{^-last}}, {{/-last}}{{/parentRequiredVars}});
{{/parent}}
{{#vars}}
{{^vendorExtensions.x-custom-hateoas-link}}
{{#required}}
{{#openApiNullable}}
this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.ofNullable({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}};
{{/openApiNullable}}
{{^openApiNullable}}
this.{{name}} = {{name}};
{{/openApiNullable}}
{{/required}}
{{/vendorExtensions.x-custom-hateoas-link}}
{{/vars}}
}
{{/hasRequired}}
{{/generatedConstructorWithRequiredArgs}}
{{/lombok.RequiredArgsConstructor}}
{{/lombok.Data}}
{{#vars}}
{{^vendorExtensions.x-custom-hateoas-link}}
{{^lombok.Data}}
{{! begin feature: fluent setter methods }}
public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {
{{#openApiNullable}}
this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.of({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}};
{{/openApiNullable}}
{{^openApiNullable}}
this.{{name}} = {{name}};
{{/openApiNullable}}
return this;
}
{{#isArray}}
public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
{{#openApiNullable}}
if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) {
this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}{{#isNullable}}){{/isNullable}};
}
this.{{name}}{{#isNullable}}.get(){{/isNullable}}.add({{name}}Item);
{{/openApiNullable}}
{{^openApiNullable}}
if (this.{{name}} == null) {
this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}};
}
this.{{name}}.add({{name}}Item);
{{/openApiNullable}}
return this;
}
{{/isArray}}
{{#isMap}}
public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
{{#openApiNullable}}
if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) {
this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}HashMap{{/uniqueItems}}<>(){{/defaultValue}}{{#isNullable}}){{/isNullable}};
}
this.{{name}}{{#isNullable}}.get(){{/isNullable}}.put(key, {{name}}Item);
{{/openApiNullable}}
{{^openApiNullable}}
if (this.{{name}} == null) {
this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}HashMap{{/uniqueItems}}<>(){{/defaultValue}};
}
this.{{name}}.put(key, {{name}}Item);
{{/openApiNullable}}
return this;
}
{{/isMap}}
{{! end feature: fluent setter methods }}
{{! begin feature: getter and setter }}
{{^lombok.Getter}}
/**
{{#description}}
* {{{.}}}
{{/description}}
{{^description}}
* Get {{name}}
{{/description}}
{{#minimum}}
* minimum: {{.}}
{{/minimum}}
{{#maximum}}
* maximum: {{.}}
{{/maximum}}
* @return {{name}}
{{#deprecated}}
* @deprecated
{{/deprecated}}
*/
{{#vendorExtensions.x-extra-annotation}}
{{{vendorExtensions.x-extra-annotation}}}
{{/vendorExtensions.x-extra-annotation}}
{{#useBeanValidation}}
{{>beanValidation}}
{{/useBeanValidation}}
{{^useBeanValidation}}
{{#required}}@NotNull{{/required}}
{{/useBeanValidation}}
{{#swagger2AnnotationLibrary}}
@Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}})
{{/swagger2AnnotationLibrary}}
{{#swagger1AnnotationLibrary}}
@ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")
{{/swagger1AnnotationLibrary}}
{{#jackson}}
@JsonProperty("{{baseName}}")
{{#withXml}}
@JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}")
{{/withXml}}
{{/jackson}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
public {{>nullableDataTypeBeanValidation}} {{getter}}() {
return {{name}};
}
{{/lombok.Getter}}
{{^lombok.Setter}}
{{#deprecated}}
/**
* @deprecated
*/
{{/deprecated}}
{{#vendorExtensions.x-setter-extra-annotation}}
{{{vendorExtensions.x-setter-extra-annotation}}}
{{/vendorExtensions.x-setter-extra-annotation}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
public void {{setter}}({{>nullableDataType}} {{name}}) {
this.{{name}} = {{name}};
}
{{/lombok.Setter}}
{{/lombok.Data}}
{{! end feature: getter and setter }}
{{/vendorExtensions.x-custom-hateoas-link}}
{{/vars}}
{{>additionalProperties}}
{{^lombok.Data}}
{{#parentVars}}
{{^lombok.Setter}}
{{! begin feature: fluent setter methods for inherited properties }}
public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {
super.{{name}}({{name}});
return this;
}
{{#isArray}}
public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
super.add{{nameInPascalCase}}Item({{name}}Item);
return this;
}
{{/isArray}}
{{#isMap}}
public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
super.put{{nameInPascalCase}}Item(key, {{name}}Item);
return this;
}
{{/isMap}}
{{/lombok.Setter}}
{{! end feature: fluent setter methods for inherited properties }}
{{/parentVars}}
{{^lombok.ToString}}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class {{classname}} {\n");
{{#parent}}
sb.append(" ").append(toIndentedString(super.toString())).append("\n");
{{/parent}}
{{#vars}}{{^vendorExtensions.x-custom-hateoas-link}}
sb.append(" {{name}}: ").append({{#isPassword}}
"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n");
{{/vendorExtensions.x-custom-hateoas-link}}{{/vars}}{{#additionalPropertiesType}}
sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n");
{{/additionalPropertiesType}}sb.append("}");
return sb.toString();
}
/**
* Convert the given object to string with each line indented by 4 spaces
* (except the first line).
*/
private String toIndentedString(Object o) {
if (o == null) {
return "null";
}
return o.toString().replace("\n", "\n ");
}
{{/lombok.ToString}}
{{/lombok.Data}}
}
I also removed the "lombok.EqualsAndHashCode
" parts and simply added the lombok annotation "@lombok.EqualsAndHashCode(callSuper = true)
" by using the "configOptions.set([additionalModelTypeAnnotations : "@lombok.Builder @lombok.EqualsAndHashCode(callSuper = true)])
" provided by the gradle OpenAPI generator plugin.
Then in my OpenAPI documentation i added the "x-custom-hateoas-link: true
" parameter to the "_links
" objects presents in the schemas section like that :
schemas:
Context:
type: object
description: Context response object
required:
- "_embedded"
- "_links"
properties:
"_embedded":
type: object
readOnly: true
required:
- default_data
properties:
default_data:
$ref: "#/components/schemas/DefaultData"
"_links":
x-custom-hateoas-link: true
type: object
readOnly: true
required:
- self
properties:
self:
description: Link to this resource
type: object
required:
- href
properties:
href:
type: string
example: 'uri to your resource'
I also made other configurations to avoid the generation of the "ContextLinks" class by using the ".openapi-generator-ignore" configuration file and used the "spotless" gradle plugin to remove the unused imports which was causing compilation error because the classes wasn't anymore generated ...
I didn't find a way to use the model.mustache
template to do not generate the imports of the classes that was specified in the ".openapi-generator-ignore" configuration file ...
Here is the configuration of the OpenAPI gradle plugin :
openApiGenerate {
// https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
generatorName.set("spring")
inputSpec.set("$rootDir/src/main/resources/api/openapi.yml")
outputDir.set(layout.buildDirectory.file("generated").get().toString())
modelPackage.set("you.package.generated.adapter.rest.openapi")
// https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
templateDir.set("$rootDir/src/main/resources/api/templates")
ignoreFileOverride.set("$rootDir/src/main/resources/api/templates/.openapi-generator-ignore")
// https://openapi-generator.tech/docs/globals/
globalProperties.set([verbose : "false",
apis : "false",
apiTests : "false",
invokers : "false",
modelDocs: "false",
models : ""
])
// https://openapi-generator.tech/docs/generators/spring
configOptions.set([additionalModelTypeAnnotations : "@lombok.Builder @lombok.EqualsAndHashCode(callSuper = true) @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)",
dateLibrary : "java8",
disallowAdditionalPropertiesIfNotPresent: "false",
openApiNullable : "false",
useSwaggerUI : "false",
useOptional : "true",
useSpringBoot3 : "true",
documentationProvider : "none",
generatedConstructorWithRequiredArgs : "false",
serializableModel : "true",
hateoas : "true"])
modelNameSuffix.set("Model")
}
And the configuration of the "spotless" plugin :
spotless {
java {
target "build/generated/src/main/java/**"
removeUnusedImports()
}
}
compileJava {
sourceSets.main.java.srcDirs += layout.buildDirectory.file("generated/src/main/java").get().toString()
}
tasks.spotlessJava.dependsOn(tasks.openApiGenerate)
compileJava.dependsOn(tasks.spotlessJavaApply)
The explanation can be confusing so if you have questions, do not hesitate :)
Here are the resources that helped me to set up this workaround :
And voilà, have fun !🎉
.openapi-generator-ignore
what it fix?
.openapi-generator-ignore
what it fix?
This is not a mandatory configuration to be able to use "links" named fields in the OpenAPI specification.
But this is to avoid the generation of useless classes which are created by the OpenAPI generator because the "_links" field is an object:
"_links":
x-custom-hateoas-link: true
type: object
Here is the content of my ".openapi-generator-ignore" file :
# OpenAPI Generator Ignore
# Exclude HATEOAS Links related classes
**LinksModel.java
**LinksSelfModel.java
**LinksFirstModel.java
**LinksLastModel.java
**LinksNextModel.java
**LinksPrevModel.java
mmm so you dont use the
there is no issue to choose what component object we want to extends Representation model ?
like add conf ? (thanks for your conf with this i was able to add jackson annotations and on serialization at least the links disappeared)
Bug Report Checklist
Description
Main openapi objective described here: https://spec.openapis.org/oas/v3.1.0.html#abstract includes:
But if we decide to use
spring
generator and opt-inhateoas
things get complicated.If we go with basic definition of API spec (simplified pet store) & config - placed below. Then we got API that compiles 🥳, but at the same time is undiscoverable 😭.
Because if we do "implement"
findPetsByStatus
like thisreturn ResponseEntity.ok().body(List.of(new Pet()));
then such request:curl -X GET -i 'http://localhost:8080/pet/findByStatus?status=available'
returns[{"links":[],"id":null,"name":null,"category":null,"photoUrls":[],"tags":null,"status":null}]
.(yeah I know this is NOT HAL representation, but probably can be changed by spring configuration) Now we have some part of return value (
links
) not documented in openapi. But as we like api-first approach and generating client and server from one definition then only one option remains. Add Links to definition:just add:
to pet properties, and components:
And we will get:
If I understand correctly here we should be rescued by https://openapi-generator.tech/docs/usage/#type-mappings-and-import-mappings
but when we add to config:
We ended up with API that compiles 🥳 and is discoverable 🥳. Sadly 😭now we have
generated as
Pet
model butRepresentationModel
has itself:Now "builder" API of
Pet
mixes up withRepresentationModel
API and nothing is straightforward for now on. Even worseRepresentationModel
API is useless as its property (links
) won't end up in JSON.openapi-generator version
Both docker's
v6.1.0
andlatest
(master
)OpenAPI declaration file content or url
https://gist.github.com/plblueraven/9d844001829d3548f62990cd41a24392 - basic https://gist.github.com/plblueraven/f1e0481427ae99f980e6d96d963ce845 - with links
Generation Details
Generate server using
docker run --rm -v "${PWD}/${generatedApiDirectoryRelativeLocation}:/generated-api" -v "${PWD}/${apiDefinitionFileRelativeLocation}:/spec/api.yaml" -v "${PWD}/${apiGeneratorConfigFileRelativeLocation}:/config.yaml" openapitools/openapi-generator-cli:latest generate -c /config.yaml
With such (effectively)
config.yaml
:or this one:
Steps to reproduce
Generate server with above config and later go to generated directory and run
mvn spring-boot:run
.Related issues/PRs
1130
Suggest a fix
Make
hateoas
option in spring generator compatible with main openapi objective? IMO way that it should be done needs discussion.