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
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.ByteArraySchema;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.DateTimeLocalSchema;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.DurationSchema;
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.JsonSchema;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.media.TimeLocalSchema;
import io.swagger.v3.oas.models.media.TimeSchema;
import io.swagger.v3.oas.models.media.UUIDSchema;
import org.apache.commons.lang3.StringUtils;

Expand Down Expand Up @@ -221,6 +225,46 @@ public Schema createProperty31() {
return new JsonSchema().typesItem("string").format("partial-time");
}
},
DATE_TIME_LOCAL(java.time.LocalDateTime.class, "date-time-local") {
@Override
public Schema createProperty() {
return new DateTimeLocalSchema();
}
@Override
public Schema createProperty31() {
return new JsonSchema().typesItem("string").format("date-time-local");
}
},
TIME(java.time.OffsetTime.class, "time") {
@Override
public Schema createProperty() {
return new TimeSchema();
}
@Override
public Schema createProperty31() {
return new JsonSchema().typesItem("string").format("time");
}
},
TIME_LOCAL(java.time.LocalTime.class, "time-local") {
@Override
public Schema createProperty() {
return new TimeLocalSchema();
}
@Override
public Schema createProperty31() {
return new JsonSchema().typesItem("string").format("time-local");
}
},
DURATION(java.time.Duration.class, "duration") {
@Override
public Schema createProperty() {
return new DurationSchema();
}
@Override
public Schema createProperty31() {
return new JsonSchema().typesItem("string").format("duration");
}
},
FILE(java.io.File.class, "file") {
@Override
public FileSchema createProperty() {
Expand Down Expand Up @@ -315,6 +359,10 @@ public Schema createProperty31() {
dms.put("string_uuid", "uuid");
dms.put("string_date", "date");
dms.put("string_date-time", "date-time");
dms.put("string_date-time-local", "date-time-local");
dms.put("string_time", "time");
dms.put("string_time-local", "time-local");
dms.put("string_duration", "duration");
dms.put("string_partial-time", "partial-time");
dms.put("string_password", "password");
dms.put("boolean_", "boolean");
Expand Down Expand Up @@ -361,6 +409,8 @@ public Schema createProperty31() {
"org.joda.time.ReadableDateTime",
"org.joda.time.DateTime",
"java.time.Instant");
addKeys(externalClasses, TIME, "java.time.OffsetTime");
addKeys(externalClasses, DURATION, "java.time.Duration");
EXTERNAL_CLASSES = Collections.unmodifiableMap(externalClasses);

final Map<String, PrimitiveType> names = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
Expand Down Expand Up @@ -582,9 +632,33 @@ private DateStub() {
* See https://xml2rfc.tools.ietf.org/public/rfc/html/rfc3339.html#anchor14
*
* @since 2.0.6
* @deprecated Use {@link #enableJava8Formats()} instead, which maps {@code java.time.LocalTime}
* to the OpenAPI Formats Registry format {@code "time-local"}.
* This method will be removed in the next major version.
*/
@Deprecated
public static void enablePartialTime() {
customClasses().put("org.joda.time.LocalTime", PrimitiveType.PARTIAL_TIME);
customClasses().put("java.time.LocalTime", PrimitiveType.PARTIAL_TIME);
}

/**
* Opts in to the OpenAPI Formats Registry mappings for Java 8 date/time types:
* <ul>
* <li>{@code java.time.LocalDateTime} → format {@code "date-time-local"}</li>
* <li>{@code java.time.LocalTime} → format {@code "time-local"}</li>
* </ul>
* {@code java.time.OffsetTime} and {@code java.time.Duration} are already mapped
* by default to {@code "time"} and {@code "duration"} respectively, since their
* previous expansion as complex objects was always incorrect.
*
* <p>Note: {@code java.time.LocalDateTime} defaults to {@code "date-time"} for
* backward compatibility. The default will change in the next major version.
*
* @since 2.2.51
*/
public static void enableJava8Formats() {
customClasses().put("java.time.LocalDateTime", PrimitiveType.DATE_TIME_LOCAL);
customClasses().put("java.time.LocalTime", PrimitiveType.TIME_LOCAL);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package io.swagger.v3.core.resolving;

import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.matchers.SerializationMatchers;
import io.swagger.v3.core.resolving.resources.TestObjectJava8Dates;
import io.swagger.v3.core.resolving.resources.TestObject2992;
import io.swagger.v3.core.util.PrimitiveType;
import org.testng.annotations.Test;

import java.util.Map;

/**
* Verifies Java 8 date/time type → OpenAPI format mappings (issue #5172).
*
* Default behaviour (backward-compatible):
* OffsetTime → "time" (fixed; was incorrectly a complex object)
* Duration → "duration" (fixed; was incorrectly a complex object)
* LocalDateTime → "date-time" (unchanged for compatibility)
* LocalTime → complex object (unchanged; call enableJava8Formats() to opt in)
*
* Opt-in via PrimitiveType.enableJava8Formats():
* LocalDateTime → "date-time-local"
* LocalTime → "time-local"
*/
public class Java8DateFormatsTest extends SwaggerTestBase {

@Test
public void testDefaultFormats() throws Exception {
final ModelResolver modelResolver = new ModelResolver(mapper());
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

context.resolve(new AnnotatedType(TestObjectJava8Dates.class));

SerializationMatchers.assertEqualsToYaml(context.getDefinedModels(), "TestObjectJava8Dates:\n" +
" type: object\n" +
" properties:\n" +
" localDateTime:\n" +
" type: string\n" +
" format: date-time\n" +
" offsetDateTime:\n" +
" type: string\n" +
" format: date-time\n" +
" zonedDateTime:\n" +
" type: string\n" +
" format: date-time\n" +
" instant:\n" +
" type: string\n" +
" format: date-time\n" +
" localDate:\n" +
" type: string\n" +
" format: date\n" +
" offsetTime:\n" +
" type: string\n" +
" format: time\n" +
" duration:\n" +
" type: string\n" +
" format: duration");
}

@Test
public void testEnableJava8Formats() throws Exception {
// Save current state so other tests are not affected by the static customClasses map
final Map<String, PrimitiveType> custom = PrimitiveType.customClasses();
final PrimitiveType prevLocalDateTime = custom.get("java.time.LocalDateTime");
final PrimitiveType prevLocalTime = custom.get("java.time.LocalTime");

PrimitiveType.enableJava8Formats();
try {
final ModelResolver modelResolver = new ModelResolver(mapper());
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

context.resolve(new AnnotatedType(TestObject2992.class));

// LocalDateTime → "date-time-local", LocalTime → "time-local" after opt-in
SerializationMatchers.assertEqualsToYaml(context.getDefinedModels(), "TestObject2992:\n" +
" type: object\n" +
" properties:\n" +
" name:\n" +
" type: string\n" +
" a:\n" +
" type: string\n" +
" format: time-local\n" +
" b:\n" +
" type: string\n" +
" format: time-local\n" +
" c:\n" +
" type: string\n" +
" format: time-local\n" +
" d:\n" +
" type: string\n" +
" format: date-time-local\n" +
" e:\n" +
" type: string\n" +
" format: date-time-local\n" +
" f:\n" +
" type: string\n" +
" format: date-time-local");
} finally {
// Restore previous state so subsequent tests are not affected
if (prevLocalDateTime == null) custom.remove("java.time.LocalDateTime");
else custom.put("java.time.LocalDateTime", prevLocalDateTime);
if (prevLocalTime == null) custom.remove("java.time.LocalTime");
else custom.put("java.time.LocalTime", prevLocalTime);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.swagger.v3.core.resolving.resources;

import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZonedDateTime;

// LocalTime is intentionally excluded: its default behaviour depends on whether
// enablePartialTime() has been called by a preceding test (shared static state).
// LocalTime opt-in behaviour is covered in Java8DateFormatsTest#testEnableJava8Formats.
public class TestObjectJava8Dates {

private LocalDateTime localDateTime;
private OffsetDateTime offsetDateTime;
private ZonedDateTime zonedDateTime;
private Instant instant;
private LocalDate localDate;
private OffsetTime offsetTime;
private Duration duration;

public LocalDateTime getLocalDateTime() { return localDateTime; }
public void setLocalDateTime(LocalDateTime localDateTime) { this.localDateTime = localDateTime; }

public OffsetDateTime getOffsetDateTime() { return offsetDateTime; }
public void setOffsetDateTime(OffsetDateTime offsetDateTime) { this.offsetDateTime = offsetDateTime; }

public ZonedDateTime getZonedDateTime() { return zonedDateTime; }
public void setZonedDateTime(ZonedDateTime zonedDateTime) { this.zonedDateTime = zonedDateTime; }

public Instant getInstant() { return instant; }
public void setInstant(Instant instant) { this.instant = instant; }

public LocalDate getLocalDate() { return localDate; }
public void setLocalDate(LocalDate localDate) { this.localDate = localDate; }

public OffsetTime getOffsetTime() { return offsetTime; }
public void setOffsetTime(OffsetTime offsetTime) { this.offsetTime = offsetTime; }

public Duration getDuration() { return duration; }
public void setDuration(Duration duration) { this.duration = duration; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.swagger.v3.oas.models.media;

import java.time.LocalDateTime;
import java.util.Objects;

/**
* DateTimeLocalSchema
*/
public class DateTimeLocalSchema extends Schema<LocalDateTime> {

public DateTimeLocalSchema() {
super("string", "date-time-local");
}

@Override
public DateTimeLocalSchema type(String type) {
super.setType(type);
return this;
}

@Override
public DateTimeLocalSchema format(String format) {
super.setFormat(format);
return this;
}

@Override
protected LocalDateTime cast(Object value) {
if (value != null) {
try {
if (value instanceof LocalDateTime) {
return (LocalDateTime) value;
} else if (value instanceof String) {
return LocalDateTime.parse((String) value);
}
} catch (Exception e) {
}
}
return null;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
return super.equals(o);
}

@Override
public int hashCode() {
return Objects.hash(super.hashCode());
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("class DateTimeLocalSchema {\n");
sb.append(" ").append(toIndentedString(super.toString())).append("\n");
sb.append("}");
return sb.toString();
}
}
Loading