Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add in Optional handling for SpringBoot POJOs - #11384

Closed
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability
Closed

Add in Optional handling for SpringBoot POJOs#11384
welshm wants to merge 33 commits into
OpenAPITools:masterfrom
welshm:spring_oas3_nullability

Conversation

@welshm

@welshmwelshm commented Jan 23, 2022

Copy link
Copy Markdown
Contributor

Modified pojo.mustache to have Optional be used on POJOs

NOTE: This is a breaking change for anyone with useOptional enabled.

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package ./bin/generate-samples.sh
    ./bin/utils/export_docs_generators.sh
    
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
  • File the PR against the correct branch: master (5.3.0), 6.0.x
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

Spec that shows the use case

ApiDefault:
type: object
required:
- requiredMessage
- requiredNullableMessage
properties:
message:
type: string
description: A message
nullableMessage:
type: string
description: A nullable message
nullable: true
requiredMessage:
type: string
description: A required message
requiredNullableMessage:
type: string
description: A required nullable message
nullable: true

Output

useOptional: false
openApiNullable: false
private String message;
private String nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: true
openApiNullable: false
private Optional<String> message;
private Optional<String> nullableMessage
private String requiredMessage;
private String requiredNullableMessage;
useOptional: false
openApiNullable: true
private String message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;
useOptional: true
openApiNullable: true
private Optional<String> message;
private JsonNullable<String> nullableMessage
private String requiredMessage;
private JsonNullable<String> requiredNullableMessage;

@@ -0,0 +1,12 @@
generatorName: spring
outputDir: samples/openapi3/server/petstore/springboot-nullable-disabled
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore-with-nullable-required.yaml

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This validates against 3_0

@welshm

Copy link
Copy Markdown
ContributorAuthor

@wing328@cachescrubber

@bbdouglas (2017/07) @sreeshas (2017/08) @jfiala (2017/08) @lukoyanov (2017/09) @cbornet (2017/09) @jeff9finger (2018/01) @karismann (2019/03) @Zomzog (2019/04) @lwlee2608 (2019/10)

@wing328

wing328 commented Feb 12, 2022

Copy link
Copy Markdown
Member

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

@welshm

Copy link
Copy Markdown
ContributorAuthor

NOTE: This is a breaking change for anyone with useOptional enabled.

Thanks for the PR. Is it correct to say that the uesOptional option will work as expected after this PR gets merged? If other words, this PR aims to fix issues when the useOptional option is set to true, right?

That is my opinion on how useOptional should function, yes. I will post the issue to the Slack to see if anyone has any differing opinions

- FOO
- BAR
default: BAR
Params:

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelleD I tried to cover all relevant cases I could think of here

@welshm

Copy link
Copy Markdown
ContributorAuthor

A quick test shows this to be working with both enabled:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JsonNullableModule());
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

However, I don't see nullability being enforced - if I don't set a required field (such as requiredNumber) this will still parse to/from JSON no problem. This is unrelated to Optional work here.

<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.9.6</version>
</dependency>
{{/useOptional}}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About the maven plug-in test failure, I think you will also need to update modules/openapi-generator-maven-plugin/examples/spring.xml

Test: https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/openapi-generator.yaml#L188

@cachescrubber

cachescrubber commented Feb 18, 2022

Copy link
Copy Markdown
Contributor

@welshm You do not need to provide the jackson-datatype-jdk8 dependency. It is provided by spring-boot-starter-json which in turn is a transitive dependency of spring-boot-starter-web . The reason it failed on the maven-plugin test is the outdated version of spring-boot used there (2.2.1.RELEASE). Just update the spring-boot version instead.

The Jackson Jdk8Module is configured by default in an recent version of spring-boot. No need to provide it as an @bean.

public class Category {

@JsonProperty("id")
private Optional<Long> id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should initialize the member with Optional.empty

A null optional field should never exists.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - fixed in next patch

private Optional<Long> id;

@JsonProperty("name")
private Optional<@Pattern(regexp = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$") String> name;

@MelleDMelleDFeb 18, 2022

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation is duplicated also in the getter. It should add to the getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... that becomes tricky because the mustache generates both as {{>optionalDataTypeWithEnum}} which internally uses {{>optionalDataType}} which has {{>beanValidationCore}} as part of the type.

While it is redundant, I am fine with the duplication for how it makes the use of the mustache simpler.

Alernatively, we could have optionalDataTypeWithEnum and annotatedOptionalDataTypeWithEnum - but would require making the same distinction for the inner usage of optionalDataType.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.
So far, optionals have only been used as parameters. I think for member variables we need a new mustache without bean validation. Because the beanvalidation is in <> always makes it awkward.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created optionalDataTypeWithoutAnnotations - I did leave it on the setter but I'm not that familiar with how the validation is applied. I can convert it to getter only if that's more correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bean validation for DTOs/Pojos have to be set on getter or fields. Currently the template set it on getter

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove from setter then


@JsonProperty("shipDate")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private Optional<OffsetDateTime> shipDate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really working with Optional?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to be from my test:

 @Test
void jdk8ModuleObjectMapper() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new Jdk8Module().configureAbsentsAsNulls(true))
.registerModule(new JavaTimeModule())
.registerModule(new JsonNullableModule())
;
final Params params = new Params()
.optionalString("OPTIONAL STRING")
.requiredString("REQUIRED STRING")
.requiredNullableString(null)
.requiredEnum(RequiredEnumEnum.ONE)
.optionalDate(LocalDate.of(2020, 10, 2))
.optionalDateTime(OffsetDateTime.of(LocalDateTime.of(2020, 10, 2, 12, 30), ZoneOffset.UTC))
.requiredNumber(123L);
final String jsonResult = objectMapper.writeValueAsString(params);
final Params readBack = objectMapper.readValue(jsonResult, Params.class);
System.console().printf(readBack.toString());
}

I can see the date time and read it back - it matches. JSON output looks fine too.

{{/swagger1AnnotationLibrary}}
public {{>nullableDataType}} {{getter}}() {
return {{name}};
public {{#isNullable}}{{>nullableDataType}}{{/isNullable}}{{^isNullable}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{#isEnum}}{{#useOptional}}Optional<{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}>{{/useOptional}}{{/isEnum}}{{^isEnum}}{{>optionalDataType}}{{/isEnum}}{{/required}}{{/isNullable}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to use an include here. The code is difficult to read and hard to understand. Isn't it possible to use optionalDataType.mustache?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems old - but I need to fix the bean annotations anyway so this might change to an include

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old, but still valid IMO

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved it's own include - I had trouble coming up with a good name so it's dataTypeDeclaration - which is kind of misleading because these are all data type declarations.

Open to suggestions of better names - and I'm taking a look at your wrapper type change now

{{/required}}
{{^required}}
private {{>optionalDataTypeWithEnum}} {{name}}{{#isNullable}} = Optional.empty(){{/isNullable}}{{^isNullable}}{{#defaultValue}} = Optional.ofNullable({{{.}}}){{/defaultValue}}{{/isNullable}};
private {{>optionalDataTypeWithEnum}} {{name}} = {{#defaultValue}}Optional.ofNullable({{{.}}}){{/defaultValue}}{{^defaultValue}}Optional.empty(){{/defaultValue}};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered to override AbstractJavaCodegen#toDefaultValue() in SpringCodegen? I would prefer to avoid logic in the templates if possible.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure I can look into that. I did it this way because that's how it was already done for JsonNullable but I'll try tk improve it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not as straight forward as one might think (but doable) I currently doing a proof of concept with JsonNullable.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might make sense to do it as a separate PR then? Consolidate the defaultValue decision and generation?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #11666

{{/isNullable}}
{{^isNullable}}
public {{#isMap}}{{>nullableDataType}}{{/isMap}}{{#isArray}}{{>nullableDataType}}{{/isArray}}{{^isMap}}{{^isArray}}{{#required}}{{>nullableDataType}}{{/required}}{{^required}}{{>optionalDataTypeWithEnum}}{{/required}}{{/isArray}}{{/isMap}} {{getter}}() {
public {{>dataTypeDeclaration}} {{getter}}() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can still be improved, but it'll be a lot easier with https://github.com/OpenAPITools/openapi-generator/pull/11666/files so I'll iterate on it after that

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you want to get #11666 in first? This change is good to go otherwise, but might be easier to deal with merge conflicts and resolve any unit tests changes if I update this after yours is in first.

@wing328wing328 modified the milestones: 6.0.0, 6.0.1May 26, 2022
@wing328wing328 modified the milestones: 6.0.1, 6.1.0Jul 5, 2022
@wing328wing328 modified the milestones: 6.1.0, 6.1.1Sep 11, 2022
@wing328wing328 modified the milestones: 6.1.1, 6.2.1Sep 24, 2022
@welshm

Copy link
Copy Markdown
ContributorAuthor

Closing - since this is way out of date

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@welshm@wing328@MelleD@cachescrubber