Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7bd73f8
useJspecify for java clients and spring generator
Mar 15, 2026
603af01
Merge master
Mar 15, 2026
7521f10
Merge branch 'master' of https://github.com/jpfinne/openapi-generator…
Mar 15, 2026
1cee79c
Merge branch 'master' into feature/jspecify
jpfinne Mar 15, 2026
9af4802
Merge remote-tracking branch 'origin/feature/jspecify' into feature/j…
Mar 15, 2026
4aae7ce
Regenerate documentation
Mar 15, 2026
8023714
Remove ignore file
Mar 15, 2026
7c4b50b
Doc only for JavaClientCodegen and SpringCodeGen
Mar 15, 2026
cca147d
Regenerate
Mar 15, 2026
c40a345
Simplification of JSpecify implementation
Mar 16, 2026
fffe2e9
Fix typo "Only supported"
Mar 16, 2026
96d0f36
Fix documentation formatting
Mar 16, 2026
4f2fdbc
Fix documentation formatting
Mar 16, 2026
b1148fb
Fix extra spaces
Mar 17, 2026
b4a4a66
Merge remote-tracking branch 'origin/master' into feature/jspecify
Mar 17, 2026
f8e0065
Merge master
Mar 17, 2026
ad2359a
Fix spacing for Spring generator
Mar 17, 2026
b2f8918
Merge branch 'master' into feature/jspecify
Mar 24, 2026
8f66a71
Merge master
Mar 24, 2026
1540fcb
Merge master
jpfinne Mar 26, 2026
cf6b820
Merge remote-tracking branch 'origin/master' into feature/jspecify
jpfinne Mar 26, 2026
bde60be
Add samples to github flow
jpfinne Mar 26, 2026
22abaf7
Merge remote-tracking branch 'origin/feature/jspecify' into feature/j…
jpfinne Mar 26, 2026
25cfde3
Try to rebuild samples on gitFlow
jpfinne Mar 26, 2026
385c2d9
Merge branch 'master' into feature/jspecify
jpfinne Mar 30, 2026
83e040b
git update-index --chmod=+x samples/client/petstore/java/native-jacks…
jpfinne Mar 31, 2026
128beb1
Add jspecify to build.gradle
jpfinne Mar 31, 2026
d2faaac
Add jspecify to build.gradle in dependencies {
jpfinne Mar 31, 2026
15f2b5d
conditionally include jspecify dependencies
jpfinne Mar 31, 2026
25b782a
Merge remote-tracking branch 'origin/master' into feature/jspecify
jpfinne Mar 31, 2026
6050184
Merge remote-tracking branch 'origin/master' into feature/jspecify
jpfinne Mar 31, 2026
c940aef
merge master
jpfinne Mar 31, 2026
fbc576d
Simplify spacing
jpfinne Apr 1, 2026
ccf5c09
Fix native request builder
jpfinne Apr 1, 2026
9e98c52
Some cleanup and comments
jpfinne Apr 2, 2026
2c4bfd4
Some cleanup
jpfinne Apr 2, 2026
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
6 changes: 6 additions & 0 deletions .github/workflows/samples-java-client-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ on:
- samples/client/petstore/java/webclient-jakarta/**
- samples/client/petstore/java/restclient-*/**
- samples/client/petstore/java/native-jackson3/**
- samples/client/petstore/java/native-jackson3-jspecify/**
- samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify/**
- samples/client/others/java/webclient-sealedInterface/**
- samples/client/others/java/webclient-sealedInterface_3_1/**
- samples/client/petstore/java/webclient-useSingleRequestParameter/**
Expand All @@ -20,6 +22,8 @@ on:
- samples/client/petstore/java/webclient-jakarta/**
- samples/client/petstore/java/restclient-*/**
- samples/client/petstore/java/native-jackson3/**
- samples/client/petstore/java/native-jackson3-jspecify/**
- samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify/**
- samples/client/others/java/webclient-sealedInterface/**
- samples/client/others/java/webclient-sealedInterface_3_1/**
- samples/client/petstore/java/webclient-useSingleRequestParameter/**
Expand All @@ -42,6 +46,8 @@ jobs:
- samples/client/petstore/java/restclient-nullable-arrays
- samples/client/petstore/java/restclient-springBoot4-jackson2
- samples/client/petstore/java/restclient-springBoot4-jackson3
- samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify
- samples/client/petstore/java/native-jackson3-jspecify
- samples/client/petstore/java/restclient-swagger2
- samples/client/petstore/java/restclient-useSingleRequestParameter
- samples/client/petstore/java/restclient-useSingleRequestParameter-static
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/samples-spring-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
- samples/client/petstore/spring-http-interface-springboot-4/**
- samples/openapi3/server/petstore/springboot-3/**
- samples/openapi3/server/petstore/springboot-4/**
- samples/openapi3/server/petstore/springboot-4-jspecify/**
- samples/server/petstore/springboot-api-response-examples/**
- samples/server/petstore/springboot-lombok-data/**
- samples/server/petstore/springboot-lombok-tostring/**
Expand All @@ -22,6 +23,7 @@ on:
- samples/client/petstore/spring-http-interface-springboot-4/**
- samples/openapi3/server/petstore/springboot-3/**
- samples/openapi3/server/petstore/springboot-4/**
- samples/openapi3/server/petstore/springboot-4-jspecify/**
- samples/server/petstore/springboot-api-response-examples/**
- samples/server/petstore/springboot-lombok-data/**
- samples/server/petstore/springboot-lombok-tostring/**
Expand All @@ -44,6 +46,7 @@ jobs:
# servers
- samples/openapi3/server/petstore/springboot-3
- samples/openapi3/server/petstore/springboot-4
- samples/openapi3/server/petstore/springboot-4-jspecify
- samples/server/petstore/springboot-api-response-examples
- samples/server/petstore/springboot-lombok-data
- samples/server/petstore/springboot-lombok-tostring
Expand Down
17 changes: 17 additions & 0 deletions bin/configs/java-native-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: java
outputDir: samples/client/petstore/java/native-jackson3-jspecify
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.

as usual, what about adding the new sample folder to the github workflow(s)?

library: native
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-native-jackson3
hideGenerationTimestamp: "true"
generateBuilders: true
useReflectionEqualsHashCode: "true"
useJackson3: "true"
openApiNullable: "false"
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
17 changes: 17 additions & 0 deletions bin/configs/java-restclient-springBoot4-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: java
outputDir: samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify
library: restclient
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-restclient
hideGenerationTimestamp: "true"
containerDefaultToNull: "true"
useSpringBoot4: true
useJackson3: true
openApiNullable: false
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
18 changes: 18 additions & 0 deletions bin/configs/java-resttemplate-springBoot4-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
generatorName: java
outputDir: samples/client/petstore/java/resttemplate-springBoot4-jackson3-jspecify
library: resttemplate
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-resttemplate
hideGenerationTimestamp: "true"
containerDefaultToNull: "true"
useJakartaEe: true
useSpringBoot4: true
useJackson3: true
openApiNullable: false
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
17 changes: 17 additions & 0 deletions bin/configs/java-webclient-springBoot4-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: java
outputDir: samples/client/petstore/java/webclient-springBoot4-jackson3-jspecify
library: webclient
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-webclient
hideGenerationTimestamp: "true"
containerDefaultToNull: "true"
useSpringBoot4: true
useJackson3: true
openApiNullable: false
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
21 changes: 21 additions & 0 deletions bin/configs/spring-boot-4-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
generatorName: spring
library: spring-boot
outputDir: samples/openapi3/server/petstore/springboot-4-jspecify
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
validateSpec: false
additionalProperties:
groupId: org.openapitools.openapi3
documentationProvider: springdoc
interfaceOnly: true
artifactId: springboot
snapshotVersion: "true"
useSpringBoot4: true
useJackson3: true
useBeanValidation: true
withXml: true
hideGenerationTimestamp: "true"
generateConstructorWithAllArgs: true
generateBuilders: true
openApiNullable: false
useJspecify: true
1 change: 1 addition & 0 deletions docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useHttpServiceProxyFactoryInterfacesConfigurator|Generate HttpInterfacesAbstractConfigurator based on an HttpServiceProxyFactory instance (as opposed to a WebClient instance, when disabled) for generating Spring HTTP interfaces.| |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|
|useJspecify|Use Jspecify for null checks| |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|
|useOptional|Use Optional container for optional parameters| |false|
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/java-microprofile.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useGzipFeature|Send gzip-encoded requests| |false|
|useJackson3|Use Jackson 3 instead of Jackson 2. Supported for 'native' library (requires Java 17+) and for Spring 'resttemplate', 'webclient', and 'restclient' libraries (require useSpringBoot4=true). Incompatible with 'openApiNullable'.| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useJspecify|Use Jspecify for null checks. Only supported for [native, restclient, resttemplate, webclient]| |false|
|useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped. Only jersey2, jersey3, native, okhttp-gson support this option.| |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| |false|
|usePlayWS|Use Play! Async HTTP client (Play WS API)| |false|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useGzipFeature|Send gzip-encoded requests| |false|
|useJackson3|Use Jackson 3 instead of Jackson 2. Supported for 'native' library (requires Java 17+) and for Spring 'resttemplate', 'webclient', and 'restclient' libraries (require useSpringBoot4=true). Incompatible with 'openApiNullable'.| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useJspecify|Use Jspecify for null checks. Only supported for [native, restclient, resttemplate, webclient]| |false|
|useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped. Only jersey2, jersey3, native, okhttp-gson support this option.| |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| |false|
|usePlayWS|Use Play! Async HTTP client (Play WS API)| |false|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useHttpServiceProxyFactoryInterfacesConfigurator|Generate HttpInterfacesAbstractConfigurator based on an HttpServiceProxyFactory instance (as opposed to a WebClient instance, when disabled) for generating Spring HTTP interfaces.| |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|
|useJspecify|Use Jspecify for null checks| |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|
|useOptional|Use Optional container for optional parameters| |false|
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ protected ImmutableMap.Builder<String, Lambda> addMustacheLambdas() {
.put("indented_8", new IndentedLambda(8, " ", false, false))
.put("indented_12", new IndentedLambda(12, " ", false, false))
.put("indented_16", new IndentedLambda(16, " ", false, false))
.put("trim", new TrimLambda())
.put("trimLineBreaks", new TrimLineBreaksLambda())
.put("trimWhitespace", new TrimWhitespaceLambda())
.put("trimTrailingWithNewLine", new TrimTrailingWhiteSpaceLambda(true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
Expand Down Expand Up @@ -58,6 +60,8 @@
import javax.lang.model.SourceVersion;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
Expand Down Expand Up @@ -104,6 +108,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
public static final String IMPLICIT_HEADERS_REGEX = "implicitHeadersRegex";
public static final String JAVAX_PACKAGE = "javaxPackage";
public static final String USE_JAKARTA_EE = "useJakartaEe";
public static final String USE_JSPECIFY = "useJspecify";
public static final String CONTAINER_DEFAULT_TO_NULL = "containerDefaultToNull";
public static final String DISABLE_DISCRIMINATOR_JSON_IGNORE_PROPERTIES = "disableDiscriminatorJsonIgnoreProperties";

Expand Down Expand Up @@ -216,6 +221,11 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original}
*/
@Getter @Setter
protected boolean useBeanValidation = false;
@Getter
@Setter
protected boolean useJspecify;
protected JSpecifyNullableLambda jSpecifyNullableLambda;

private Map<String, String> schemaKeyToModelNameCache = new HashMap<>();

public AbstractJavaCodegen() {
Expand Down Expand Up @@ -597,6 +607,7 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(CAMEL_CASE_DOLLAR_SIGN, this::setCamelCaseDollarSign);
convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces);
convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming);
convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJspecify);

if (!StringUtils.isEmpty(parentGroupId) && !StringUtils.isEmpty(parentArtifactId) && !StringUtils.isEmpty(parentVersion)) {
additionalProperties.put("parentOverridden", true);
Expand Down Expand Up @@ -847,6 +858,20 @@ protected void applyJakartaPackage() {
writePropertyBack(JAVAX_PACKAGE, "jakarta");
}

protected void applyJspecify() {
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.

no need to set the JAVAX_PACKAGE to jspecify?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry, obsolete commit, I've removed it

@Chrimle thanks for the review

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.

Okay, so there's no need for something like writePropertyBack(JAVAX_PACKAGE, "jspecify")?

Resolve if no concern 👍

importMapping.put("Nullable", "org.jspecify.annotations.Nullable");
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.

please add a docstring to this new function explaining what it goes.

if (Boolean.TRUE.equals(additionalProperties.get(CodegenConstants.GENERATE_MODELS))) {
supportingFiles.add(new SupportingFile("modelPackageInfo.mustache",
(sourceFolder + File.separator + modelPackage).replace(".", java.io.File.separator),
"package-info.java"));
}
if (Boolean.TRUE.equals(additionalProperties.get(CodegenConstants.GENERATE_APIS))) {
supportingFiles.add(new SupportingFile("apiPackageInfo.mustache",
(sourceFolder + File.separator + apiPackage).replace(".", java.io.File.separator),
"package-info.java"));
}
}

@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
Expand Down Expand Up @@ -2653,4 +2678,78 @@ public void setEnumPropertyNaming(final String enumPropertyNamingType) {
throw new RuntimeException(sb.toString());
}
}

@Override
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
this.jSpecifyNullableLambda = new JSpecifyNullableLambda();
// Add jSpecify nullable annotation in the correct location before or inside a declartion
Mustache.Lambda jSpecifyDatatypeLambda = (fragment, writer) -> {
String dataType = fragment.execute();
if (jSpecifyNullableLambda.keptNullable) {
jSpecifyNullableLambda.keptNullable = false;
int idx = dataType.lastIndexOf('.');
if (idx > 0) {
// generate declaration like java.time.@Nullable Timestamp
writer.write(dataType.substring(0, idx + 1));
writer.write("@Nullable ");
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.

The trailing space in "@Nullable " won't cause double-spaces?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We want the space between @Nullable and Timestamp, no?

I've found a possible improvement in the java pojo.mustache templates.
There is an extra space in generated code like
void setDate( java.time.@Nullable Timestamp dt)
I'll try to remove 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.

aha, nice find!

I just had a gut feeling that "@Nullable " would be used in a context like:
{{>nullableThing}} {{{variableName}}}, so it would end up with double spaces.
But if that's not the case, then my comment is invalid 👍

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.

To be fair, there's a LOT of formatting issues in pretty much all generators. I use mustache-comments quite a lot to make it structured/readable, but also keeping the formatting. example:
https://github.com/Chrimle/openapi-to-java-records-mustache-templates/blob/1638990570757734ef71dd1d05aade4998b43ab5/mustache-templates/src/main/resources/templates/useBeanValidation.mustache#L38-L52

so, if it's an easy fix, why not 😉

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.

... but if it's not - don't bother with it right now 😉

writer.write(dataType.substring(idx + 1));
} else {
if (dataType.startsWith(" ")) {
writer.write(" @Nullable");
writer.write(dataType);
} else {
writer.write("@Nullable ");
writer.write(dataType);
}
}
} else {
writer.write(dataType);
}
};
return super.addMustacheLambdas()
.put("jSpecifyDatatype", jSpecifyDatatypeLambda)
.put("jSpecifyNullable", jSpecifyNullableLambda);

}

/**
* for Jspecify, remove @Nullable before the datatype and set keptNullable to true if done.
*/
class JSpecifyNullableLambda implements Mustache.Lambda {
private String nullableAnnotation = "@Nullable";
// remember if @Nullable is needed
boolean keptNullable = false;

public void setNullableAnnotation(String nullableAnnotation) {
this.nullableAnnotation = nullableAnnotation;
}

@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
keptNullable = false;
String value = fragment.execute();
if (useJspecify) {
if (value.startsWith(nullableAnnotation)) {
keptNullable = true;
int idx = nullableAnnotation.length();
// trim left
while (idx < value.length() && value.charAt(idx) == ' ') {
idx ++;
}
value = value.substring(idx);
}
}
writer.write(value);
}
}

/**
* Adds Nullable import if any parameter is nullable or optional.
*/
protected void addNullableImportForOperation(CodegenOperation codegenOperation) {
codegenOperation.allParams.stream()
.filter(CodegenParameter::notRequiredOrIsNullable)
.findAny()
.ifPresent(param -> codegenOperation.imports.add("Nullable"));
}
}
Loading
Loading