From 97fe053a8110f274cb0624186b8a2f74ddeb2da1 Mon Sep 17 00:00:00 2001 From: asimon Date: Wed, 15 Apr 2026 12:37:14 +0200 Subject: [PATCH] [java] Support 'time-local' format See https://spec.openapis.org/registry/format/time-local.html --- .../languages/AbstractJavaCodegen.java | 12 ++++++++ .../codegen/utils/ModelUtils.java | 6 ++++ .../codegen/java/AbstractJavaCodegenTest.java | 21 ++++++++++--- .../java/spring/SpringCodegenTest.java | 29 ++++++++++++++++++ .../date-time-parameter-types-for-testing.yml | 4 +++ .../main/java/org/openapitools/model/Pet.java | 30 +++++++++++++++++-- .../main/java/org/openapitools/model/Pet.java | 30 +++++++++++++++++-- 7 files changed, 124 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java index 58f7d0e1f226..5d2954765468 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java @@ -661,6 +661,7 @@ public void processOpts() { additionalProperties.put("jsr310", "true"); typeMapping.put("date", "LocalDate"); importMapping.put("LocalDate", "java.time.LocalDate"); + typeMapping.put("time-local","LocalTime"); importMapping.put("LocalTime", "java.time.LocalTime"); if ("java8-localdatetime".equals(dateLibrary)) { typeMapping.put("DateTime", "LocalDateTime"); @@ -1411,6 +1412,13 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) { return "URI.create(\"" + escapeText(String.valueOf(schema.getDefault())) + "\")"; } return null; + } else if (ModelUtils.isTimeLocalSchema(schema)) { + if (schema.getDefault() != null) { + if ("java8".equals(getDateLibrary())) { + return String.format(Locale.ROOT, "LocalTime.parse(\"%s\")", schema.getDefault()); + } + } + return null; } else if (ModelUtils.isStringSchema(schema)) { if (schema.getDefault() != null) { String _default; @@ -1493,6 +1501,10 @@ public String toDefaultValue(CodegenProperty cp, Schema schema) { value.asText(), "java.time.format.DateTimeFormatter.ISO_ZONED_DATE_TIME.withZone(java.time.ZoneId.systemDefault())"); } + } else if(ModelUtils.isTimeLocalSchema(propertySchema)) { + if("java8".equals(getDateLibrary())) { + defaultPropertyExpression = String.format(Locale.ROOT, "java.time.LocalTime.parse(\"%s\")", value.asText()); + } } else if(ModelUtils.isUUIDSchema(propertySchema)) { defaultPropertyExpression = "java.util.UUID.fromString(\"" + value.asText() + "\")"; } else if(ModelUtils.isStringSchema(propertySchema)) { diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 84d8cf484f73..086456d0dbcd 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -696,6 +696,12 @@ public static boolean isDateTimeSchema(Schema schema) { && SchemaTypeUtil.DATE_TIME_FORMAT.equals(schema.getFormat())); } + public static boolean isTimeLocalSchema(Schema schema) { + // format: time-local, see https://spec.openapis.org/registry/format/time-local.html + return (SchemaTypeUtil.STRING_TYPE.equals(getType(schema)) + && "time-local".equals(schema.getFormat())); + } + public static boolean isPasswordSchema(Schema schema) { return (schema instanceof PasswordSchema) || // double diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java index 0de76c2eed8a..fa9bbb3375ce 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/AbstractJavaCodegenTest.java @@ -38,10 +38,7 @@ import java.io.File; import java.math.BigDecimal; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneId; -import java.time.ZonedDateTime; +import java.time.*; import java.util.*; import java.util.stream.Collectors; @@ -531,6 +528,15 @@ public void toDefaultValueDateTimeLegacyTest() { // dateLibrary <> java8 Assert.assertEquals(defaultValue, "1984-12-19T03:39:57-09:00"); + + // Test default value for time-local format + StringSchema timeLocalSchema = new StringSchema(); + timeLocalSchema.setFormat("time-local"); + timeLocalSchema.setDefault(LocalTime.parse("10:15:30")); + defaultValue = codegen.toDefaultValue(timeLocalSchema); + + // dateLibrary <> java8 + Assert.assertEquals(defaultValue, "10:15:30"); } @Test @@ -593,6 +599,13 @@ public void toDefaultValueTest() { numberSchema.setFormat("double"); defaultValue = codegen.toDefaultValue(codegen.fromProperty("", schema), numberSchema); Assert.assertEquals(defaultValue, doubleValue + "d"); + + // Test default value for time-local format + StringSchema timeLocalSchema = new StringSchema(); + timeLocalSchema.setFormat("time-local"); + timeLocalSchema.setDefault("10:15:30"); + defaultValue = codegen.toDefaultValue(codegen.fromProperty("", timeLocalSchema), timeLocalSchema); + Assert.assertEquals(defaultValue, "LocalTime.parse(\"10:15:30\")"); } @Test diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java index 6dc57b29ad10..708b186e14e3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/spring/SpringCodegenTest.java @@ -381,6 +381,35 @@ public void generateFormatForDateAndDateTimeQueryParam() throws IOException { .containsWithNameAndAttributes("DateTimeFormat", ImmutableMap.of("iso", "DateTimeFormat.ISO.DATE_TIME")); } + @Test + public void generateLocalTimeForTimeLocalFormat() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/spring/date-time-parameter-types-for-testing.yml", null, new ParseOptions()).getOpenAPI(); + + SpringCodegen codegen = new SpringCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGenerateMetadata(false); + generator.opts(input).generate(); + + JavaFileAssert.assertThat(Paths.get(outputPath + "/src/main/java/org/openapitools/model/Pet.java")) + .hasImports("java.time.LocalTime") + .assertProperty("feedingTime").withType("LocalTime"); + } + @Test public void interfaceDefaultImplDisableWithResponseWrapper() { final SpringCodegen codegen = new SpringCodegen(); diff --git a/modules/openapi-generator/src/test/resources/3_0/spring/date-time-parameter-types-for-testing.yml b/modules/openapi-generator/src/test/resources/3_0/spring/date-time-parameter-types-for-testing.yml index 535d152fc96e..cfc47f06afe8 100644 --- a/modules/openapi-generator/src/test/resources/3_0/spring/date-time-parameter-types-for-testing.yml +++ b/modules/openapi-generator/src/test/resources/3_0/spring/date-time-parameter-types-for-testing.yml @@ -102,3 +102,7 @@ components: type: string format: date default: '2021-01-01' + feedingTime: + type: string + format: time-local + default: '10:15:30' \ No newline at end of file diff --git a/samples/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java b/samples/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java index abb5ef9adbf5..ed52171f4406 100644 --- a/samples/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java +++ b/samples/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalTime; import java.time.OffsetDateTime; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.lang.Nullable; @@ -40,6 +41,8 @@ public class Pet { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate dateOfBirth = LocalDate.parse("2021-01-01"); + private LocalTime feedingTime = LocalTime.parse("10:15:30"); + public Pet() { super(); } @@ -177,6 +180,27 @@ public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; } + public Pet feedingTime(LocalTime feedingTime) { + this.feedingTime = feedingTime; + return this; + } + + /** + * Get feedingTime + * @return feedingTime + */ + @Valid + @Schema(name = "feedingTime", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty("feedingTime") + public LocalTime getFeedingTime() { + return feedingTime; + } + + @JsonProperty("feedingTime") + public void setFeedingTime(LocalTime feedingTime) { + this.feedingTime = feedingTime; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -191,12 +215,13 @@ public boolean equals(Object o) { Objects.equals(this.happy, pet.happy) && Objects.equals(this.price, pet.price) && Objects.equals(this.lastFeed, pet.lastFeed) && - Objects.equals(this.dateOfBirth, pet.dateOfBirth); + Objects.equals(this.dateOfBirth, pet.dateOfBirth) && + Objects.equals(this.feedingTime, pet.feedingTime); } @Override public int hashCode() { - return Objects.hash(atType, age, happy, price, lastFeed, dateOfBirth); + return Objects.hash(atType, age, happy, price, lastFeed, dateOfBirth, feedingTime); } @Override @@ -209,6 +234,7 @@ public String toString() { sb.append(" price: ").append(toIndentedString(price)).append("\n"); sb.append(" lastFeed: ").append(toIndentedString(lastFeed)).append("\n"); sb.append(" dateOfBirth: ").append(toIndentedString(dateOfBirth)).append("\n"); + sb.append(" feedingTime: ").append(toIndentedString(feedingTime)).append("\n"); sb.append("}"); return sb.toString(); } diff --git a/samples/openapi3/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java b/samples/openapi3/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java index abb5ef9adbf5..ed52171f4406 100644 --- a/samples/openapi3/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java +++ b/samples/openapi3/client/petstore/spring-cloud-date-time/src/main/java/org/openapitools/model/Pet.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalTime; import java.time.OffsetDateTime; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.lang.Nullable; @@ -40,6 +41,8 @@ public class Pet { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) private LocalDate dateOfBirth = LocalDate.parse("2021-01-01"); + private LocalTime feedingTime = LocalTime.parse("10:15:30"); + public Pet() { super(); } @@ -177,6 +180,27 @@ public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; } + public Pet feedingTime(LocalTime feedingTime) { + this.feedingTime = feedingTime; + return this; + } + + /** + * Get feedingTime + * @return feedingTime + */ + @Valid + @Schema(name = "feedingTime", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty("feedingTime") + public LocalTime getFeedingTime() { + return feedingTime; + } + + @JsonProperty("feedingTime") + public void setFeedingTime(LocalTime feedingTime) { + this.feedingTime = feedingTime; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -191,12 +215,13 @@ public boolean equals(Object o) { Objects.equals(this.happy, pet.happy) && Objects.equals(this.price, pet.price) && Objects.equals(this.lastFeed, pet.lastFeed) && - Objects.equals(this.dateOfBirth, pet.dateOfBirth); + Objects.equals(this.dateOfBirth, pet.dateOfBirth) && + Objects.equals(this.feedingTime, pet.feedingTime); } @Override public int hashCode() { - return Objects.hash(atType, age, happy, price, lastFeed, dateOfBirth); + return Objects.hash(atType, age, happy, price, lastFeed, dateOfBirth, feedingTime); } @Override @@ -209,6 +234,7 @@ public String toString() { sb.append(" price: ").append(toIndentedString(price)).append("\n"); sb.append(" lastFeed: ").append(toIndentedString(lastFeed)).append("\n"); sb.append(" dateOfBirth: ").append(toIndentedString(dateOfBirth)).append("\n"); + sb.append(" feedingTime: ").append(toIndentedString(feedingTime)).append("\n"); sb.append("}"); return sb.toString(); }