Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
|useJSpecify|Use JSpecify's @Nullable (org.jspecify.annotations.Nullable) instead of Spring's @Nullable. Overrides any user-supplied importMapping for 'Nullable'.| |false|
|useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |true|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
|useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
|useJSpecify|Use JSpecify's @Nullable (org.jspecify.annotations.Nullable) instead of Spring's @Nullable. Overrides any user-supplied importMapping for 'Nullable'.| |false|
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 13, 2026

Choose a reason for hiding this comment

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

P2: useJSpecify is documented and generates JSpecify annotation usage, but Spring build templates do not appear to add the required org.jspecify:jspecify dependency, risking uncompilable generated projects.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/generators/spring.md, line 105:

<comment>`useJSpecify` is documented and generates JSpecify annotation usage, but Spring build templates do not appear to add the required `org.jspecify:jspecify` dependency, risking uncompilable generated projects.</comment>

<file context>
@@ -102,6 +102,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
 |useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
 |useFeignClientContextId|Whether to generate Feign client with contextId parameter.| |true|
 |useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
+|useJSpecify|Use JSpecify's @Nullable (org.jspecify.annotations.Nullable) instead of Spring's @Nullable. Overrides any user-supplied importMapping for 'Nullable'.| |false|
 |useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
 |useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
</file context>
Fix with Cubic

|useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |true|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public class SpringCodegen extends AbstractJavaCodegen
public static final String JACKSON3_PACKAGE = "tools.jackson";
public static final String JACKSON_PACKAGE = "jacksonPackage";
public static final String ADDITIONAL_NOT_NULL_ANNOTATIONS = "additionalNotNullAnnotations";
public static final String USE_JSPECIFY = "useJSpecify";

@Getter
public enum RequestMappingMode {
Expand Down Expand Up @@ -183,6 +184,8 @@ public enum RequestMappingMode {
protected boolean useJackson3 = false;
@Getter @Setter
protected boolean additionalNotNullAnnotations = false;
@Getter @Setter
protected boolean useJSpecify = false;

public SpringCodegen() {
super();
Expand Down Expand Up @@ -330,6 +333,10 @@ public SpringCodegen() {
cliOptions.add(CliOption.newBoolean(ADDITIONAL_NOT_NULL_ANNOTATIONS,
"Add @NotNull to path variables (required by default) and requestBody.",
additionalNotNullAnnotations));
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY,
"Use JSpecify's @Nullable (org.jspecify.annotations.Nullable) instead of Spring's @Nullable. " +
"Overrides any user-supplied importMapping for 'Nullable'.",
isUseJSpecify()));

}

Expand Down Expand Up @@ -545,16 +552,21 @@ public void processOpts() {
convertPropertyToStringAndWriteBack(RESOURCE_FOLDER, this::setResourceFolder);

convertPropertyToBooleanAndWriteBack(ADDITIONAL_NOT_NULL_ANNOTATIONS, this::setAdditionalNotNullAnnotations);
convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJSpecify);

// override parent one
importMapping.put("JsonDeserialize", (useJackson3 ? JACKSON3_PACKAGE : JACKSON2_PACKAGE) + ".databind.annotation.JsonDeserialize");

typeMapping.put("file", "org.springframework.core.io.Resource");
importMapping.put("Nullable", "org.springframework.lang.Nullable");
importMapping.put("org.springframework.core.io.Resource", "org.springframework.core.io.Resource");
importMapping.put("DateTimeFormat", "org.springframework.format.annotation.DateTimeFormat");
importMapping.put("ApiIgnore", "springfox.documentation.annotations.ApiIgnore");
importMapping.put("ParameterObject", "org.springdoc.api.annotations.ParameterObject");
if (isUseJSpecify()) {
importMapping.put("Nullable", "org.jspecify.annotations.Nullable");
} else {
importMapping.put("Nullable", "org.springframework.lang.Nullable");
}
if (isUseSpringBoot3() || isUseSpringBoot4()) {
importMapping.put("ParameterObject", "org.springdoc.core.annotations.ParameterObject");
}
Expand Down Expand Up @@ -1019,10 +1031,39 @@ public void setParameterExampleValue(CodegenParameter p) {
}
}

/**
* Returns true if the nullableAnnotation.mustache partial would emit {@code @Nullable}
* for this property.
*/
private boolean willEmitNullableAnnotation(CodegenProperty property) {
if (property.required) return false;
if (property.defaultValue == null) {
if (useOptional) return false;
if (isOpenApiNullable()) return !property.isNullable;
return true;
} else {
if (isOpenApiNullable()) return false;
return property.isNullable;
}
}

@Override
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
super.postProcessModelProperty(model, property);

if (isUseJSpecify() && !property.isContainer && willEmitNullableAnnotation(property)) {
String datatype = property.datatypeWithEnum;
int lastDot = datatype.lastIndexOf('.');
if (lastDot >= 0) {
// Insert annotation between package path and simple class name: a.b.c.@Nullable TypeName
property.vendorExtensions.put("x-jspecify-annotated-type",
datatype.substring(0, lastDot + 1) + "@Nullable " + datatype.substring(lastDot + 1));
} else {
// No package qualifier – annotation before type is still valid
property.vendorExtensions.put("x-jspecify-annotated-type", "@Nullable " + datatype);
}
}

// add org.springframework.format.annotation.DateTimeFormat when needed
if (property.isDate || property.isDateTime) {
model.imports.add("DateTimeFormat");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
{{#useJSpecify}}
<!-- JSpecify @Nullable annotation -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
{{/useJSpecify}}
{{#withXml}}
<!-- XML processing: Jackson -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
{{#useJSpecify}}
<!-- JSpecify @Nullable annotation -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
{{/useJSpecify}}
{{#withXml}}
<!-- XML processing: Jackson -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@
<version>3.0.2</version>
{{/parentOverridden}}
</dependency>
{{#useJSpecify}}
<!-- JSpecify @Nullable annotation -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
{{/useJSpecify}}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@
<version>3.0.2</version>
{{/parentOverridden}}
</dependency>
{{#useJSpecify}}
<!-- JSpecify @Nullable annotation -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
{{/useJSpecify}}
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@
<artifactId>jsr305</artifactId>
<version>3.0.2</version>
</dependency>
{{#useJSpecify}}
<!-- JSpecify @Nullable annotation -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
</dependency>
{{/useJSpecify}}
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}
{{/openApiNullable}}
{{^openApiNullable}}
private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
private {{#vendorExtensions.x-jspecify-annotated-type}}{{{.}}}{{/vendorExtensions.x-jspecify-annotated-type}}{{^vendorExtensions.x-jspecify-annotated-type}}{{>nullableAnnotation}}{{>nullableDataType}}{{/vendorExtensions.x-jspecify-annotated-type}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
{{/openApiNullable}}
{{/isContainer}}
{{^isContainer}}
Expand All @@ -84,7 +84,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
private {{>nullableAnnotation}}{{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#useOptional}} = Optional.{{^defaultValue}}empty(){{/defaultValue}}{{#defaultValue}}of({{{.}}}){{/defaultValue}};{{/useOptional}}{{^useOptional}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/useOptional}}{{/isNullable}}{{/required}}{{^isNullable}}{{#required}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/required}}{{/isNullable}}
{{/openApiNullable}}
{{^openApiNullable}}
private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
{{#vendorExtensions.x-jspecify-annotated-type}}private {{{vendorExtensions.x-jspecify-annotated-type}}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};{{/vendorExtensions.x-jspecify-annotated-type}}{{^vendorExtensions.x-jspecify-annotated-type}}private {{>nullableAnnotation}}{{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};{{/vendorExtensions.x-jspecify-annotated-type}}
{{/openApiNullable}}
{{/isContainer}}
{{/vars}}
Expand Down Expand Up @@ -144,7 +144,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
{{^lombok.Data}}

{{! begin feature: fluent setter methods }}
public {{classname}} {{name}}({{>nullableAnnotation}}{{{datatypeWithEnum}}} {{name}}) {
public {{classname}} {{name}}({{#vendorExtensions.x-jspecify-annotated-type}}{{{vendorExtensions.x-jspecify-annotated-type}}}{{/vendorExtensions.x-jspecify-annotated-type}}{{^vendorExtensions.x-jspecify-annotated-type}}{{>nullableAnnotation}}{{{datatypeWithEnum}}}{{/vendorExtensions.x-jspecify-annotated-type}} {{name}}) {
{{#openApiNullable}}
this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.of{{#optionalAcceptNullable}}Nullable{{/optionalAcceptNullable}}({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}};
{{/openApiNullable}}
Expand Down Expand Up @@ -229,7 +229,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
{{#jackson}}{{>jackson_annotations}}{{/jackson}}{{#withXml}}{{>xmlAccessorAnnotation}}{{/withXml}} public {{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}} {{getter}}() {
{{#jackson}}{{>jackson_annotations}}{{/jackson}}{{#withXml}}{{>xmlAccessorAnnotation}}{{/withXml}} public {{#vendorExtensions.x-jspecify-annotated-type}}{{{vendorExtensions.x-jspecify-annotated-type}}}{{/vendorExtensions.x-jspecify-annotated-type}}{{^vendorExtensions.x-jspecify-annotated-type}}{{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}}{{/vendorExtensions.x-jspecify-annotated-type}} {{getter}}() {
return {{name}};
}
{{/lombok.Getter}}
Expand All @@ -246,7 +246,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{>jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{>nullableAnnotation}}{{>nullableDataType}} {{name}}) {
{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{>jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{#vendorExtensions.x-jspecify-annotated-type}}{{{vendorExtensions.x-jspecify-annotated-type}}}{{/vendorExtensions.x-jspecify-annotated-type}}{{^vendorExtensions.x-jspecify-annotated-type}}{{>nullableAnnotation}}{{>nullableDataType}}{{/vendorExtensions.x-jspecify-annotated-type}} {{name}}) {
this.{{name}} = {{name}};
}
{{/lombok.Setter}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6637,4 +6637,35 @@ public void shouldNotHaveDocumentationAnnotationWhenUsingLibrarySpringHttpInterf
JavaFileAssert.assertThat(Paths.get(outputPath + "/src/main/java/org/openapitools/api/PetApi.java"))
.assertMethod("addPet").assertParameter("pet").assertParameterAnnotations().doesNotContainWithName("Parameter");
}

@Test
public void testIssue23206() throws IOException {
final SpringCodegen codegen = new SpringCodegen();

codegen.setLibrary(SPRING_BOOT);

codegen.schemaMapping().put("SchemaMappedDatatype", "a.b.c.SchemaMappedDatatype");

codegen.additionalProperties().put(OPENAPI_NULLABLE, "false");
codegen.additionalProperties().put(SKIP_DEFAULT_INTERFACE, "true");
codegen.additionalProperties().put(USE_SPRING_BOOT4, "true");
codegen.additionalProperties().put(USE_JACKSON_3, "true");
codegen.additionalProperties().put(USE_TAGS, "true");
codegen.additionalProperties().put(USE_BEANVALIDATION, "false");
codegen.additionalProperties().put(USE_JSPECIFY, "true");

final Map<String, File> files = generateFiles(codegen, "src/test/resources/3_0/spring/issue_23206.yaml");
final var javaFileAssert = JavaFileAssert.assertThat(files.get("SchemaMappedWithTypeUseAnnotationDto.java"));
javaFileAssert
.hasImports("org.jspecify.annotations.Nullable")
.assertProperty("schemaMappedDatatype");

javaFileAssert.fileContains("private a.b.c.@Nullable SchemaMappedDatatype schemaMappedDatatype = null;");
javaFileAssert.fileContains("public a.b.c.@Nullable SchemaMappedDatatype getSchemaMappedDatatype() {");
javaFileAssert.fileContains("public void setSchemaMappedDatatype(a.b.c.@Nullable SchemaMappedDatatype schemaMappedDatatype) {");

javaFileAssert.fileContains("private @Nullable BigDecimal importedDatatype = null;");
javaFileAssert.fileContains("public @Nullable BigDecimal getImportedDatatype() {");
javaFileAssert.fileContains("public void setImportedDatatype(@Nullable BigDecimal importedDatatype) {");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: "3.0.3"
info:
title: Move Annotations in Generated POJOs
description: "Test Placement of Annotations, to allow TYPE_USE Annotations to compile correctly. See #23206"
version: 1.0.0

paths:
/some/endpoint:
get:
responses:
"200":
description: OK

components:
schemas:
SchemaMappedWithTypeUseAnnotationDto:
type: object
properties:
schemaMappedDatatype:
$ref: '#/components/schemas/SchemaMappedDatatype'
importedDatatype:
$ref: '#/components/schemas/ImportedDatatype'
SchemaMappedDatatype:
nullable: true
type: number
ImportedDatatype:
nullable: true
type: number