Skip to content

Commit 5713f30

Browse files
authored
feat: add JSpecify nullness annotations (#390)
1 parent 40126bb commit 5713f30

File tree

154 files changed

+441
-157
lines changed

Some content is hidden

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

154 files changed

+441
-157
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
- Update to okhttp `5.3.2`
99
- Requiring Java 21
1010
- Remove implicit grant flow (see [blog post from Spotify](https://developer.spotify.com/blog/2025-10-14-reminder-oauth-migration-27-nov-2025))
11+
- Add JSpecify nullness annotations
1112

1213
## [4.3.2]
1314
- Remove `followers` property from `PlaylistUser` object

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262

6363
<!-- Dependencies -->
6464
<lombok.version>1.18.42</lombok.version>
65+
<jspecify.version>1.0.0</jspecify.version>
6566

6667
<!-- Test Dependencies -->
6768
<junit.version>5.13.4</junit.version>

spotify-web-api-java-generator/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
</properties>
3232

3333
<dependencies>
34+
<dependency>
35+
<groupId>org.jspecify</groupId>
36+
<artifactId>jspecify</artifactId>
37+
<version>${jspecify.version}</version>
38+
</dependency>
3439
<dependency>
3540
<groupId>io.swagger.parser.v3</groupId>
3641
<artifactId>swagger-parser</artifactId>

spotify-web-api-java-generator/src/main/java/de/sonallux/spotify/generator/java/CLI.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public class CLI implements Runnable {
3232
boolean shouldClean = false;
3333

3434
@Option(names = {"-p", "--package"}, required = true, description = "The Java package name")
35-
String packageName = null;
35+
String packageName;
3636

3737
@Option(names = { "-h", "--help" }, usageHelp = true, description = "Print usage help")
3838
boolean helpRequested = false;

spotify-web-api-java-generator/src/main/java/de/sonallux/spotify/generator/java/JavaGenerator.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import io.swagger.v3.oas.models.OpenAPI;
77
import lombok.extern.slf4j.Slf4j;
88

9-
import java.io.IOException;
109
import java.nio.file.Path;
1110

1211
@Slf4j
@@ -17,7 +16,7 @@ public JavaGenerator() {
1716
this.mustacheFactory = new NoEscapingMustacheFactory();
1817
}
1918

20-
public void generate(OpenAPI openAPI, Path outputDirectory, JavaPackage javaPackage) throws IOException, GeneratorException {
19+
public void generate(OpenAPI openAPI, Path outputDirectory, JavaPackage javaPackage) throws GeneratorException {
2120
var generationContext = new GenerationContext(this.mustacheFactory, openAPI, javaPackage, outputDirectory);
2221

2322
new BaseObjectGenerator(generationContext).generateBaseObject();
@@ -30,8 +29,7 @@ public void generate(OpenAPI openAPI, Path outputDirectory, JavaPackage javaPack
3029

3130
EndpointSplitter.splitEndpoints(spotifyWebApi);
3231

33-
var apiTemplate = new ApiGenerator(generationContext);
34-
apiTemplate.generateEndpoints(spotifyWebApi);
32+
ApiGenerator.generateEndpoints(generationContext, spotifyWebApi);
3533

3634
new SpotifyWebApiGenerator(generationContext).generate();
3735

spotify-web-api-java-generator/src/main/java/de/sonallux/spotify/generator/java/generators/ApiGenerator.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import de.sonallux.spotify.generator.java.util.JavaPackage;
1010
import de.sonallux.spotify.generator.java.util.JavaUtils;
1111
import de.sonallux.spotify.generator.java.util.Markdown2Html;
12+
import lombok.AccessLevel;
1213
import lombok.RequiredArgsConstructor;
1314

1415
import java.util.*;
@@ -17,16 +18,17 @@
1718
import static java.util.stream.Collectors.joining;
1819
import static java.util.stream.Collectors.toList;
1920

20-
@RequiredArgsConstructor
21+
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
2122
public class ApiGenerator {
2223
private final GenerationContext generationContext;
2324

24-
private JavaPackage apisJavaPackage;
25+
private final JavaPackage apisJavaPackage;
2526

26-
public void generateEndpoints(SpotifyWebApi spotifyWebApi) {
27-
apisJavaPackage = generationContext.childPackage("apis");
27+
public static void generateEndpoints(GenerationContext generationContext, SpotifyWebApi spotifyWebApi) {
28+
final var apisJavaPackage = generationContext.childPackage("apis");
29+
final var apiGenerator = new ApiGenerator(generationContext, apisJavaPackage);
2830

29-
spotifyWebApi.getCategories().forEach(this::generateApiClasses);
31+
spotifyWebApi.getCategories().forEach(apiGenerator::generateApiClasses);
3032
}
3133

3234
private void generateApiClasses(ApiCategory category) {

spotify-web-api-java-generator/src/main/java/de/sonallux/spotify/generator/java/generators/ObjectGenerator.java

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.swagger.v3.oas.models.media.ComposedSchema;
1111
import io.swagger.v3.oas.models.media.ObjectSchema;
1212
import io.swagger.v3.oas.models.media.Schema;
13+
import org.jspecify.annotations.Nullable;
1314

1415
import java.nio.file.Path;
1516
import java.util.HashMap;
@@ -41,6 +42,7 @@ public ObjectGenerator(GenerationContext generationContext) {
4142
this.schemaNameToObjectName = new ConcurrentHashMap<>();
4243
}
4344

45+
@Nullable
4446
public String getObjectNameForResponse(String responseName) {
4547
return responseNameToObjectName.get(responseName);
4648
}
@@ -71,78 +73,80 @@ public void generateAllObjects() {
7173
* @param schema the OpenAPI schema
7274
* @return an object name to use for this schema
7375
*/
74-
private String generateApiObject(String openApiName, Schema schema) {
76+
private String generateApiObject(String openApiName, Schema<?> schema) {
7577
if (schema.get$ref() != null) {
7678
var schemaName = OpenApiUtils.getSchemaName(schema.get$ref());
7779
return getObjectNameOrGenerate(schemaName, generationContext.resolveSchema(schema.get$ref()));
7880
}
7981

8082
var objectName = getObjectNameFromSchemaName(openApiName);
8183

82-
if (schema instanceof ObjectSchema objectSchema) {
83-
var apiObject = generateApiObject(objectSchema, objectName);
84-
apiObject.setOpenApiName(openApiName);
85-
return objectName;
86-
}
87-
if (schema instanceof ArraySchema arraySchema) {
88-
var itemsSchema = arraySchema.getItems();
89-
var itemsType = JavaUtils.getTypeOfSchema(itemsSchema).orElse("Object");
90-
91-
return "java.util.List<" + itemsType + ">";
92-
}
93-
if (schema instanceof ComposedSchema composedSchema) {
94-
if (composedSchema.getAllOf() != null) {
95-
var allOf = composedSchema.getAllOf();
96-
if (allOf.size() == 1) {
97-
var ref = allOf.getFirst().get$ref();
98-
if (ref.equals("#/components/schemas/PagingObject")) {
99-
var itemsSchema = (ArraySchema) composedSchema.getProperties().get("items");
100-
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
101-
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
102-
return "Paging<" + itemsObjectName + ">";
103-
}
104-
105-
var referencedSchemaName = OpenApiUtils.getSchemaName(ref);
106-
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.getFirst());
107-
var apiObject = ApiObject.builder()
84+
switch (schema) {
85+
case ObjectSchema objectSchema -> {
86+
var apiObject = generateApiObject(objectSchema, objectName);
87+
apiObject.setOpenApiName(openApiName);
88+
return objectName;
89+
}
90+
case ArraySchema arraySchema -> {
91+
var itemsSchema = arraySchema.getItems();
92+
var itemsType = JavaUtils.getTypeOfSchema(itemsSchema).orElse("Object");
93+
return "java.util.List<" + itemsType + ">";
94+
}
95+
case ComposedSchema composedSchema -> {
96+
if (composedSchema.getAllOf() != null) {
97+
var allOf = composedSchema.getAllOf();
98+
if (allOf.size() == 1) {
99+
var ref = allOf.getFirst().get$ref();
100+
if (ref.equals("#/components/schemas/PagingObject")) {
101+
var itemsSchema = (ArraySchema) composedSchema.getProperties().get("items");
102+
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
103+
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
104+
return "Paging<" + itemsObjectName + ">";
105+
}
106+
107+
var referencedSchemaName = OpenApiUtils.getSchemaName(ref);
108+
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.getFirst());
109+
var apiObject = ApiObject.builder()
108110
.name(objectName)
109111
.openApiName(openApiName)
110112
.superClassName(referencedObjectName)
111113
.description(composedSchema.getDescription())
112114
.build();
113-
this.schemaObjects.put(objectName, apiObject);
114-
return objectName;
115-
}
116-
if (allOf.size() == 2) {
117-
if (allOf.get(0).get$ref().equals("#/components/schemas/PagingObject")) {
118-
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
119-
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
120-
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
121-
return "Paging<" + itemsObjectName + ">";
122-
}
123-
if (allOf.get(0).get$ref().equals("#/components/schemas/CursorPagingObject")) {
124-
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
125-
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
126-
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
127-
return "CursorPaging<" + itemsObjectName + ">";
128-
}
129-
130-
if (allOf.get(1) instanceof ObjectSchema objectSchema) {
131-
var referencedSchemaName = OpenApiUtils.getSchemaName(allOf.get(0).get$ref());
132-
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.get(0));
133-
134-
var apiObject = generateApiObject(objectSchema, objectName);
135-
apiObject.setDescription(composedSchema.getDescription());
136-
apiObject.setOpenApiName(openApiName);
137-
apiObject.setSuperClassName(referencedObjectName);
115+
this.schemaObjects.put(objectName, apiObject);
138116
return objectName;
139117
}
118+
if (allOf.size() == 2) {
119+
if (allOf.get(0).get$ref().equals("#/components/schemas/PagingObject")) {
120+
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
121+
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
122+
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
123+
return "Paging<" + itemsObjectName + ">";
124+
}
125+
if (allOf.get(0).get$ref().equals("#/components/schemas/CursorPagingObject")) {
126+
var itemsSchema = (ArraySchema) allOf.get(1).getProperties().get("items");
127+
var itemsSchemaName = OpenApiUtils.getSchemaName(itemsSchema.getItems().get$ref());
128+
var itemsObjectName = getObjectNameOrGenerate(itemsSchemaName, itemsSchema.getItems());
129+
return "CursorPaging<" + itemsObjectName + ">";
130+
}
131+
132+
if (allOf.get(1) instanceof ObjectSchema objectSchema) {
133+
var referencedSchemaName = OpenApiUtils.getSchemaName(allOf.getFirst().get$ref());
134+
var referencedObjectName = getObjectNameOrGenerate(referencedSchemaName, allOf.getFirst());
135+
136+
var apiObject = generateApiObject(objectSchema, objectName);
137+
apiObject.setDescription(composedSchema.getDescription());
138+
apiObject.setOpenApiName(openApiName);
139+
apiObject.setSuperClassName(referencedObjectName);
140+
return objectName;
141+
}
142+
}
140143
}
144+
return objectName;
145+
}
146+
default -> {
147+
return objectName;
141148
}
142149
}
143-
144-
145-
return objectName;
146150
}
147151

148152
private ApiObject generateApiObject(ObjectSchema objectSchema, String objectName) {
@@ -180,7 +184,7 @@ private ApiObject.Property generateApiObjectProperty(String objectName, String n
180184
return new ApiObject.Property(name, type, resolvedSchema.getDescription());
181185
}
182186

183-
private String getObjectNameOrGenerate(String openApiName, Schema schema) {
187+
private String getObjectNameOrGenerate(String openApiName, Schema<?> schema) {
184188
if (schemaNameToObjectName.containsKey(openApiName)) {
185189
return schemaNameToObjectName.get(openApiName);
186190
}
@@ -260,7 +264,7 @@ private void fixContextForPaging(Map<String, Object> context) {
260264
));
261265
}
262266

263-
private static String firstNonNull(String... strings) {
267+
private static @Nullable String firstNonNull(@Nullable String... strings) {
264268
for (var s : strings) {
265269
if (s != null) {
266270
return s;

spotify-web-api-java-generator/src/main/java/de/sonallux/spotify/generator/java/generators/ObjectModelCreator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@
1313
import io.swagger.v3.oas.models.media.Schema;
1414
import io.swagger.v3.oas.models.responses.ApiResponse;
1515
import lombok.RequiredArgsConstructor;
16+
import org.jspecify.annotations.Nullable;
1617

1718
import java.util.function.Function;
1819

1920
@RequiredArgsConstructor
2021
public class ObjectModelCreator {
2122
private final GenerationContext generationContext;
22-
private final Function<String, String> responseTypeMapper;
23+
private final Function<String, @Nullable String> responseTypeMapper;
2324

24-
private SpotifyWebApi spotifyWebApi;
25+
private SpotifyWebApi spotifyWebApi = new SpotifyWebApi();
2526

2627
public SpotifyWebApi createSpotifyWebApiModel(OpenAPI openAPI) {
2728
spotifyWebApi = new SpotifyWebApi();
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@NullMarked
2+
package de.sonallux.spotify.generator.java.generators;
3+
4+
import org.jspecify.annotations.NullMarked;

spotify-web-api-java-generator/src/main/java/de/sonallux/spotify/generator/java/model/ApiEndpoint.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import de.sonallux.spotify.generator.java.util.Markdown2Html;
55
import lombok.Getter;
66
import lombok.Setter;
7+
import org.jspecify.annotations.Nullable;
78

89
import java.util.ArrayList;
910
import java.util.List;
@@ -25,7 +26,7 @@ public class ApiEndpoint {
2526
private final List<Parameter> optionalPathParameters;
2627
private final List<Parameter> optionalQueryParameters;
2728
private final List<Parameter> optionalBodyParameters;
28-
private RawBodyParameter rawBodyParameter = null;
29+
private @Nullable RawBodyParameter rawBodyParameter = null;
2930

3031
public ApiEndpoint(String endpointId, String name, String description, String path, String httpMethod, String responseType, String responseDescription, List<String> scopes, boolean deprecated) {
3132
this.endpointId = endpointId;
@@ -72,7 +73,7 @@ public static class Parameter {
7273
private String type;
7374
private boolean commaSeparatedListType;
7475
private String description;
75-
private String defaultValue;
76+
private @Nullable String defaultValue;
7677

7778
public Parameter(String name, String type, String description) {
7879
this.name = name;

0 commit comments

Comments
 (0)