Skip to content

Commit 9f1232e

Browse files
Ignacio-Vidalclaude
andcommitted
[Java][jaxrs-spec] add JSpecify support
jaxrs-spec had no jspecify support at all — no `useJspecify` option, and the generator emitted no nullability annotations for any library. This adds it generator-wide rather than per-library, because the model and parameter templates are shared by all six libraries (default, quarkus, thorntail, openliberty, helidon, kumuluzee); only quarkus overrides formParams. - JavaJAXRSSpecServerCodegen: `useJspecify` cliOption, applyJspecify() and the Nullable imports for models and operations. - Shared pojo.mustache (field, getter, setter, fluent setter) and the six parameter partials, plus the quarkus formParams override. - New JavaJaxRS/spec copies of modelPackageInfo/apiPackageInfo and the nullable* partials: unlike the java client libraries, which all share one `Java` template dir, jaxrs-spec has its own with no fallback. - Base pom swaps jsr305 for jspecify under the flag, matching how the java client libraries handle it; the quarkus pom adds jspecify. applyJspecify() must run *after* the `supportingFiles.clear()` in processOpts(), which would otherwise drop the @NullMarked package-info files. jaxrs-cxf-cdi extends this generator but uses its own cxf-cdi template directory, so it would have advertised the option without honouring it — the exact defect this change fixes elsewhere. It calls removeOption, alongside the existing removeOption(GENERATE_JSON_CREATOR). Every template change is guarded by {{#useJspecify}}: flag-off output is byte-identical to before, verified by diffing against a jar built from the parent branch for both the default and quarkus libraries. Adds a sample (jaxrs-spec-quarkus-jspecify, registered in samples-jdk17 since the quarkus library targets Java 17) and two tests covering the annotations, type-use placement on qualified types, and that the flag off still emits jsr305 and no annotations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BQa4EUVfxzqe4qi9PDj5RU
1 parent d7dec5c commit 9f1232e

43 files changed

Lines changed: 1658 additions & 11 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/samples-jdk17.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ on:
2424
- samples/server/petstore/java-helidon-server/v3/se/**
2525
- samples/server/petstore/jaxrs-spec-sealed/**
2626
- samples/server/petstore/jaxrs-spec/quarkus-security/**
27+
- samples/server/petstore/jaxrs-spec-quarkus-jspecify/**
2728
pull_request:
2829
paths:
2930
# clients
@@ -48,6 +49,7 @@ on:
4849
- samples/server/petstore/java-helidon-server/v3/se/**
4950
- samples/server/petstore/jaxrs-spec-sealed/**
5051
- samples/server/petstore/jaxrs-spec/quarkus-security/**
52+
- samples/server/petstore/jaxrs-spec-quarkus-jspecify/**
5153
jobs:
5254
build:
5355
name: Build with JDK17
@@ -78,6 +80,7 @@ jobs:
7880
- samples/server/petstore/java-helidon-server/v3/se
7981
- samples/server/petstore/jaxrs-spec-sealed
8082
- samples/server/petstore/jaxrs-spec/quarkus-security
83+
- samples/server/petstore/jaxrs-spec-quarkus-jspecify
8184
steps:
8285
- uses: actions/checkout@v7
8386
- uses: actions/setup-java@v6.0.0
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
generatorName: jaxrs-spec
2+
outputDir: samples/server/petstore/jaxrs-spec-quarkus-jspecify
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
5+
validateSpec: false
6+
additionalProperties:
7+
artifactId: jaxrs-spec-quarkus-jspecify
8+
library: "quarkus"
9+
hideGenerationTimestamp: "true"
10+
interfaceOnly: "true"
11+
useJakartaEe: "true"
12+
useJspecify: "true"
13+
openApiNullable: "false"
14+
dateLibrary: "java8"
15+
typeMappings:
16+
BigDecimal: java.math.BigDecimal

docs/generators/jaxrs-spec.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
8686
|useEnumCaseInsensitive|Use `equalsIgnoreCase` when String for enum comparison| |false|
8787
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
8888
|useJakartaSecurityAnnotations|Whether to generate Jakarta security annotations (@RolesAllowed, @PermitAll). Requires useJakartaEe=true. Currently only supported when library is set to quarkus.| |false|
89+
|useJspecify|Use JSpecify for null checks: @NullMarked package-info and @Nullable on optional properties and parameters.| |false|
8990
|useMicroProfileOpenAPIAnnotations|Whether to generate Microprofile OpenAPI annotations. Only valid when library is set to quarkus.| |false|
9091
|useMutiny|Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.| |false|
9192
|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|

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSCXFCDIServerCodegen.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ public JavaJAXRSCXFCDIServerCodegen() {
6464
embeddedTemplateDir = templateDir = JAXRS_TEMPLATE_DIRECTORY_NAME + File.separator + "cxf-cdi";
6565

6666
removeOption(JavaJAXRSSpecServerCodegen.GENERATE_JSON_CREATOR);
67+
// jspecify support lives in the JavaJaxRS/spec templates; this generator uses its own
68+
// cxf-cdi template directory, so the option would be advertised but have no effect.
69+
removeOption(USE_JSPECIFY);
6770
}
6871

6972
@Override

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaJAXRSSpecServerCodegen.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public JavaJAXRSSpecServerCodegen() {
171171
cliOptions.add(CliOption.newBoolean(GENERATE_JSON_CREATOR, "Whether to generate @JsonCreator constructor for required properties.", generateJsonCreator));
172172
cliOptions.add(CliOption.newBoolean(USE_ENUM_CASE_INSENSITIVE, "Use `equalsIgnoreCase` when String for enum comparison", useEnumCaseInsensitive));
173173
cliOptions.add(CliOption.newBoolean(USE_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed));
174+
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use JSpecify for null checks: @NullMarked package-info and @Nullable on optional properties and parameters.", useJspecify));
174175
}
175176

176177
@Override
@@ -255,6 +256,11 @@ public void processOpts() {
255256
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")
256257
.doNotOverwrite());
257258

259+
// after the clear() above, which would otherwise drop the @NullMarked package-info files
260+
if (useJspecify) {
261+
applyJspecify();
262+
}
263+
258264
if ((!interfaceOnly) || generateRootResources) {
259265
supportingFiles.add(new SupportingFile("RestResourceRoot.mustache",
260266
(sourceFolder + '/' + invokerPackage).replace(".", "/"), "RestResourceRoot.java")
@@ -348,6 +354,10 @@ public CodegenModel fromModel(String name, Schema model) {
348354
codegenModel.imports.remove("JsonProperty");
349355
codegenModel.imports.remove("JsonTypeName");
350356
}
357+
if (useJspecify) {
358+
codegenModel.imports.add("Nullable");
359+
}
360+
351361
return codegenModel;
352362
}
353363

@@ -481,6 +491,18 @@ public CodegenOperation fromOperation(String path, String httpMethod, Operation
481491
if (QUARKUS_LIBRARY.equals(getLibrary()) && useJakartaSecurityAnnotations) {
482492
jakartaSecurityAnnotationProcessor.applyTo(op, operation, openAPI);
483493
}
494+
if (useJspecify) {
495+
addNullableImportForOperation(op);
496+
}
484497
return op;
485498
}
499+
500+
@Override
501+
protected void applyJspecify() {
502+
super.applyJspecify();
503+
// nullable_var_annotations.mustache emits @{{javaxPackage}}.annotation.Nullable; the lambda
504+
// finds that and re-injects a bare @Nullable in type-use position, so tell it what to look for.
505+
jSpecifyNullableLambda.setNullableAnnotation("@" + additionalProperties.get(JAVAX_PACKAGE) + ".annotation.Nullable");
506+
}
507+
486508
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@org.jspecify.annotations.NullMarked
2+
package {{apiPackage}};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isBodyParam}}{{#isDeprecated}}@Deprecated {{/isDeprecated}}{{#useBeanValidation}}@Valid {{#required}}{{^isNullable}}@NotNull {{/isNullable}}{{/required}}{{/useBeanValidation}}{{{dataType}}} {{paramName}}{{/isBodyParam}}
1+
{{#isBodyParam}}{{#isDeprecated}}@Deprecated {{/isDeprecated}}{{#useBeanValidation}}@Valid {{#required}}{{^isNullable}}@NotNull {{/isNullable}}{{/required}}{{/useBeanValidation}}{{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{>nullableDataType}} {{paramName}}{{/isBodyParam}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isCookieParam}}{{#isDeprecated}}@Deprecated {{/isDeprecated}}@CookieParam("{{baseName}}"){{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{^isContainer}}{{#defaultValue}} @DefaultValue("{{{.}}}"){{/defaultValue}}{{/isContainer}} {{#useSwaggerAnnotations}}{{#description}} @ApiParam("{{.}}"){{/description}}{{/useSwaggerAnnotations}}{{#useMicroProfileOpenAPIAnnotations}}{{#description}} @org.eclipse.microprofile.openapi.annotations.parameters.Parameter(description="{{.}}"){{/description}}{{/useMicroProfileOpenAPIAnnotations}} {{{dataType}}} {{paramName}}{{/isCookieParam}}
1+
{{#isCookieParam}}{{#isDeprecated}}@Deprecated {{/isDeprecated}}@CookieParam("{{baseName}}"){{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{^isContainer}}{{#defaultValue}} @DefaultValue("{{{.}}}"){{/defaultValue}}{{/isContainer}} {{#useSwaggerAnnotations}}{{#description}} @ApiParam("{{.}}"){{/description}}{{/useSwaggerAnnotations}}{{#useMicroProfileOpenAPIAnnotations}}{{#description}} @org.eclipse.microprofile.openapi.annotations.parameters.Parameter(description="{{.}}"){{/description}}{{/useMicroProfileOpenAPIAnnotations}} {{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{>nullableDataType}} {{paramName}}{{/isCookieParam}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
{{#isFormParam}}
2-
{{#isDeprecated}}@Deprecated {{/isDeprecated}}{{^isFile}}@FormParam(value = "{{baseName}}") {{{dataType}}} {{paramName}}{{/isFile}}{{#isFile}}@FormParam(value = "{{baseName}}") InputStream {{paramName}}InputStream{{/isFile}}{{/isFormParam}}
2+
{{#isDeprecated}}@Deprecated {{/isDeprecated}}{{^isFile}}@FormParam(value = "{{baseName}}") {{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{>nullableDataType}} {{paramName}}{{/isFile}}{{#isFile}}@FormParam(value = "{{baseName}}") InputStream {{paramName}}InputStream{{/isFile}}{{/isFormParam}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#isHeaderParam}}{{#isDeprecated}}@Deprecated {{/isDeprecated}}@HeaderParam("{{baseName}}"){{#useBeanValidation}}{{>beanValidationHeaderParams}}{{/useBeanValidation}} {{#defaultValue}} @DefaultValue("{{{.}}}"){{/defaultValue}} {{#useSwaggerAnnotations}}{{#description}} @ApiParam("{{.}}"){{/description}}{{/useSwaggerAnnotations}}{{#useMicroProfileOpenAPIAnnotations}}{{#description}} @org.eclipse.microprofile.openapi.annotations.parameters.Parameter(description="{{.}}"){{/description}}{{/useMicroProfileOpenAPIAnnotations}} {{{dataType}}} {{paramName}}{{/isHeaderParam}}
1+
{{#isHeaderParam}}{{#isDeprecated}}@Deprecated {{/isDeprecated}}@HeaderParam("{{baseName}}"){{#useBeanValidation}}{{>beanValidationHeaderParams}}{{/useBeanValidation}} {{#defaultValue}} @DefaultValue("{{{.}}}"){{/defaultValue}} {{#useSwaggerAnnotations}}{{#description}} @ApiParam("{{.}}"){{/description}}{{/useSwaggerAnnotations}}{{#useMicroProfileOpenAPIAnnotations}}{{#description}} @org.eclipse.microprofile.openapi.annotations.parameters.Parameter(description="{{.}}"){{/description}}{{/useMicroProfileOpenAPIAnnotations}} {{#useJspecify}}{{>nullable_var_annotations}}{{/useJspecify}}{{>nullableDataType}} {{paramName}}{{/isHeaderParam}}

0 commit comments

Comments
 (0)