Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -12,7 +12,9 @@
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
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.EmailSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.JsonSchema;
Expand All @@ -22,6 +24,8 @@
import io.swagger.v3.oas.models.media.PasswordSchema;
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 All @@ -46,6 +50,10 @@ public class ModelDeserializer extends JsonDeserializer<Schema> {
private static final String FORMAT = "format";
private static final String DATE_FORMAT = "date";
private static final String DATE_TIME_FORMAT = "date-time";
private static final String TIME_FORMAT = "time";
private static final String DURATION_FORMAT = "duration";
private static final String DATE_TIME_LOCAL_FORMAT = "date-time-local";
private static final String TIME_LOCAL_FORMAT = "time-local";
private static final String EMAIL_FORMAT = "email";
private static final String PASSWORD_FORMAT = "password";
private static final String UUID_FORMAT = "uuid";
Expand Down Expand Up @@ -196,7 +204,15 @@ private Schema deserializeSchemaWithType(JsonNode node, JsonNode typeNode) {
schema = Json.mapper().convertValue(node, DateSchema.class);
} else if (DATE_TIME_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, DateTimeSchema.class);
} else if (EMAIL_FORMAT.equals(format)) {
} else if (TIME_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, TimeSchema.class);
} else if (DURATION_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, DurationSchema.class);
} else if (DATE_TIME_LOCAL_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, DateTimeLocalSchema.class);
} else if (TIME_LOCAL_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, TimeLocalSchema.class);
}else if (EMAIL_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, EmailSchema.class);
} else if (PASSWORD_FORMAT.equals(format)) {
schema = Json.mapper().convertValue(node, PasswordSchema.class);
Expand Down
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
Comment thread
daniel-kmiecik marked this conversation as resolved.
Outdated
*/
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; }
}
Loading
Loading