Merge pull request #686 from jamezp/issue685
[685] Ensure the value type of a container being deserialized uses th…
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 3b21094..4f5adfe 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -37,10 +37,10 @@
- name: Copyright
run: bash etc/copyright.sh
- name: Checkstyle
- run: mvn -B checkstyle:checkstyle -Pstaging
+ run: mvn -B checkstyle:checkstyle
- name: Yasson install
- run: mvn -U -C clean install -Pstaging -DskipTests
+ run: mvn -U -C clean install -DskipTests
- name: Yasson tests
- run: mvn -U -B -C -Dmaven.javadoc.skip=true -Pstaging verify
+ run: mvn -U -B -C -Dmaven.javadoc.skip=true verify
- name: JSONB-API TCK
- run: cd yasson-tck && mvn -U -B test -DargLine="-Djava.locale.providers=COMPAT" -Pstaging
+ run: cd yasson-tck && mvn -U -B test -DargLine="-Djava.locale.providers=COMPAT"
diff --git a/pom.xml b/pom.xml
index f3f048a..28c0b35 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- Copyright (c) 2016, 2024 Oracle and/or its affiliates. All rights reserved.
+ Copyright (c) 2016, 2026 Oracle and/or its affiliates. All rights reserved.
This program and the accompanying materials are made available under the
terms of the Eclipse Public License v. 2.0 which is available at
@@ -15,17 +15,17 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.eclipse.ee4j</groupId>
<artifactId>project</artifactId>
- <version>1.0.9</version>
+ <version>2.0.2</version>
</parent>
- <modelVersion>4.0.0</modelVersion>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
- <version>3.0.4-SNAPSHOT</version>
+ <version>3.0.5-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Yasson</name>
diff --git a/src/main/java/org/eclipse/yasson/internal/deserializer/types/DateDeserializer.java b/src/main/java/org/eclipse/yasson/internal/deserializer/types/DateDeserializer.java
index 723cdbe..4e3704a 100644
--- a/src/main/java/org/eclipse/yasson/internal/deserializer/types/DateDeserializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/deserializer/types/DateDeserializer.java
@@ -13,13 +13,30 @@
package org.eclipse.yasson.internal.deserializer.types;
import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.YearMonth;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
+import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.Locale;
/**
* Deserializer of the {@link Date} type.
+ *
+ * <p>
+ * For date-only patterns (e.g., "yyyy-MM-dd"), this deserializer uses {@link DateTimeFormatter#parseBest} to detect the
+ * appropriate temporal type [ZonedDateTime, LocalDateTime, LocalDate, or YearMonth]
+ * and creates the Date object at midnight in the specified
+ * timezone. When no timezone is specified in the pattern, UTC is used as required by Jakarta JSON Binding specification
+ * section 3.5.
+ * </p>
+ * <p>
+ * critical, use {@link java.time.LocalDate} (recommended) or {@link java.sql.Date}.
+ * </p>
*/
class DateDeserializer extends AbstractDateDeserializer<Date> {
@@ -45,13 +62,31 @@
}
private static Date parseWithOrWithoutZone(String jsonValue, DateTimeFormatter formatter) {
- ZonedDateTime parsed;
- if (formatter.getZone() == null) {
- parsed = ZonedDateTime.parse(jsonValue, formatter.withZone(UTC));
+ final TemporalAccessor best = formatter.parseBest(jsonValue,
+ ZonedDateTime::from,
+ LocalDateTime::from,
+ LocalDate::from,
+ YearMonth::from);
+
+ // If no zone provided in string, use the formatter's zone or UTC per the Jakarta JSON Binding specification
+ // section 3.5
+ final ZoneId zone = formatter.getZone() != null ? formatter.getZone() : ZoneOffset.UTC;
+
+ // Determine the type of the best option
+ final Instant instant;
+ if (best instanceof ZonedDateTime) {
+ instant = ((ZonedDateTime) best).toInstant();
+ } else if (best instanceof LocalDateTime) {
+ instant = ((LocalDateTime) best).atZone(zone).toInstant();
+ } else if (best instanceof LocalDate) {
+ instant = LocalDate.from(best).atStartOfDay(zone).toInstant();
+ } else if (best instanceof YearMonth) {
+ instant = ((YearMonth) best).atDay(1).atStartOfDay(zone).toInstant();
} else {
- parsed = ZonedDateTime.parse(jsonValue, formatter);
+ // Fallback
+ instant = Instant.from(best);
}
- return Date.from(parsed.toInstant());
+ return Date.from(instant);
}
}
diff --git a/src/test/java/org/eclipse/yasson/defaultmapping/dates/DatesTest.java b/src/test/java/org/eclipse/yasson/defaultmapping/dates/DatesTest.java
index 287afca..79ad21e 100644
--- a/src/test/java/org/eclipse/yasson/defaultmapping/dates/DatesTest.java
+++ b/src/test/java/org/eclipse/yasson/defaultmapping/dates/DatesTest.java
@@ -102,6 +102,10 @@
public static class SqlDateFormatted {
@JsonbDateFormat(value = "yyyy-MM-dd")
public java.sql.Date sqlDate;
+ @JsonbDateFormat(value = "yyyy-MM-dd")
+ public java.util.Date utilDate;
+
+
}
@Test
@@ -126,7 +130,101 @@
assertEquals("2018-01-31", result.sqlDate.toString());
assertEquals("2018-01-31", result.utilDate.toString());
}
-
+
+ @Test
+ public void testMarshallSqlDateFormatted() {
+ final String date = "2026-02-25";
+ final String expectedJson = String.format("{\"sqlDate\":\"%1$s\",\"utilDate\":\"%1$s\"}", date);
+
+ final SqlDateFormatted sqlDateFormatted = new SqlDateFormatted();
+ sqlDateFormatted.sqlDate = java.sql.Date.valueOf(date);
+ // We use a java.sql.Date here as we want to test as if this was a Jakarta Persistence temporal date
+ sqlDateFormatted.utilDate = java.sql.Date.valueOf(date);
+ String jsonString = bindingJsonb.toJson(sqlDateFormatted);
+ assertEquals(expectedJson, jsonString);
+
+ // Unmarshal the object
+ final SqlDateFormatted result = bindingJsonb.fromJson(jsonString, SqlDateFormatted.class);
+ assertEquals(sqlDateFormatted.sqlDate, result.sqlDate);
+ // The Date objects will not be equal unless user.timezone is set to UTC. The sqlDateFormatted.utilDate is
+ // created at midnight in the current timezone (via valueOf()), while result.utilDate is created at midnight UTC
+ // per the JSON-B specification. To verify both represent the same calendar date, we convert each to LocalDate
+ // using its respective timezone: the original uses systemDefault(), the deserialized uses UTC.
+ assertEquals(Instant.ofEpochMilli(sqlDateFormatted.utilDate.getTime()).atZone(ZoneId.systemDefault()).toLocalDate(),
+ result.utilDate.toInstant().atZone(ZoneOffset.UTC).toLocalDate());
+ }
+
+ @Test
+ public void testUnmarshallSqlDateFormatted() {
+ final String date = "2026-02-25";
+ final String expectedString = String.format("{\"sqlDate\":\"%1$s\",\"utilDate\":\"%1$s\"}", date);
+
+ final SqlDateFormatted sqlDateFormatted = bindingJsonb.fromJson(expectedString, SqlDateFormatted.class);
+ assertEquals(date, sqlDateFormatted.sqlDate.toString());
+ // Convert java.util.Date to LocalDate for comparison
+ final LocalDate resultDate = sqlDateFormatted.utilDate.toInstant()
+ .atZone(ZoneOffset.UTC)
+ .toLocalDate();
+ assertEquals(LocalDate.parse(date), resultDate);
+
+ // Unmarshal the object
+ final String result = bindingJsonb.toJson(sqlDateFormatted);
+ assertEquals(expectedString, result);
+ }
+
+ public static class YearMonthFormatted {
+ @JsonbDateFormat(value = "yyyy-MM")
+ public java.util.Date date;
+ }
+
+ @Test
+ public void testMarshallYearMonthFormat() {
+ final YearMonthFormatted yearMonthFormatted = new YearMonthFormatted();
+ yearMonthFormatted.date = java.sql.Date.valueOf("2026-02-25");
+ String jsonString = bindingJsonb.toJson(yearMonthFormatted);
+ assertEquals("{\"date\":\"2026-02\"}", jsonString);
+ }
+
+ @Test
+ public void testUnmarshallYearMonthFormat() {
+ final YearMonthFormatted yearMonthFormatted = bindingJsonb.fromJson(
+ "{\"date\":\"2026-02\"}",
+ YearMonthFormatted.class);
+ final LocalDate resultDate = yearMonthFormatted.date.toInstant()
+ .atZone(ZoneOffset.UTC)
+ .toLocalDate();
+ assertEquals(LocalDate.of(2026, 2, 1), resultDate);
+ }
+
+ @Test
+ public void testDateOnlyPatternEdgeCases() {
+ // Test various edge cases to ensure date values are preserved correctly
+ testDateRoundTrip("2028-03-01"); // Day after leap year
+ testDateRoundTrip("2026-12-31"); // Last day of year
+ testDateRoundTrip("2028-02-29"); // Leap year day
+ testDateRoundTrip("2027-01-01"); // First day of year
+ testDateRoundTrip("2028-01-31"); // Last day of January
+ testDateRoundTrip("2028-02-01"); // First day of February
+ }
+
+ private void testDateRoundTrip(final String date) {
+ final String json = String.format("{\"sqlDate\":\"%1$s\",\"utilDate\":\"%1$s\"}", date);
+
+ // Deserialize
+ final SqlDateFormatted deserialized = bindingJsonb.fromJson(json, SqlDateFormatted.class);
+
+ // Verify utilDate represents midnight UTC for the specified date
+ final LocalDate resultDate = deserialized.utilDate.toInstant()
+ .atZone(ZoneOffset.UTC)
+ .toLocalDate();
+ assertEquals(LocalDate.parse(date), resultDate, () -> String.format("Date should be %s when viewed in UTC", date));
+
+ // Verify JSON round-trip
+ final String roundTripped = bindingJsonb.toJson(deserialized);
+ assertEquals(json, roundTripped, () -> String.format("JSON should round-trip correctly for %s", date));
+ }
+
+
@Test
public void testSqlDateTimeZonesFormatted() {
testSqlDateWithTZFormatted(TimeZone.getTimeZone(ZoneId.of("Europe/Sofia")));
diff --git a/src/test/java/org/eclipse/yasson/records/CarWithGenerics.java b/src/test/java/org/eclipse/yasson/records/CarWithGenerics.java
new file mode 100644
index 0000000..619817a
--- /dev/null
+++ b/src/test/java/org/eclipse/yasson/records/CarWithGenerics.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2025 IBM and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.records;
+
+public record CarWithGenerics<T> (String type, T color) {
+}
diff --git a/src/test/java/org/eclipse/yasson/records/Color.java b/src/test/java/org/eclipse/yasson/records/Color.java
new file mode 100644
index 0000000..8744500
--- /dev/null
+++ b/src/test/java/org/eclipse/yasson/records/Color.java
@@ -0,0 +1,16 @@
+/*
+ * Copyright (c) 2025 IBM and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ */
+
+package org.eclipse.yasson.records;
+
+public record Color(String name, String code) {
+}
diff --git a/src/test/java/org/eclipse/yasson/records/RecordTest.java b/src/test/java/org/eclipse/yasson/records/RecordTest.java
index 2b6a61e..3c1b72a 100644
--- a/src/test/java/org/eclipse/yasson/records/RecordTest.java
+++ b/src/test/java/org/eclipse/yasson/records/RecordTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021, 2024 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2021, 2025 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -15,6 +15,7 @@
import jakarta.json.bind.JsonbException;
import org.eclipse.yasson.Jsonbs;
+import org.eclipse.yasson.TestTypeToken;
import org.eclipse.yasson.internal.properties.MessageKeys;
import org.eclipse.yasson.internal.properties.Messages;
import org.junit.jupiter.api.Test;
@@ -106,4 +107,16 @@
assertThrows(JsonbException.class, () -> Jsonbs.defaultJsonb.fromJson(expected, CarWithDefaultConstructor.class));
}
+ @Test
+ public void testRecordWithGenerics() {
+ CarWithGenerics<Color> car = new CarWithGenerics<>("skoda", new Color("green", "#00FF00"));
+ String expected = "{\"color\":{\"code\":\"#00FF00\",\"name\":\"green\"},\"type\":\"skoda\"}";
+
+ String json = Jsonbs.defaultJsonb.toJson(car);
+ assertThat(json, is(expected));
+
+ CarWithGenerics<Color> deserialized = Jsonbs.defaultJsonb
+ .fromJson(expected, new TestTypeToken<CarWithGenerics<Color>>() {}.getType());
+ assertThat(deserialized, is(car));
+ }
}
diff --git a/yasson-tck/pom.xml b/yasson-tck/pom.xml
index bd0d76e..c3d36c7 100644
--- a/yasson-tck/pom.xml
+++ b/yasson-tck/pom.xml
@@ -11,22 +11,13 @@
<properties>
<jsonb.tck.version>3.0.0</jsonb.tck.version>
- <yasson.version>3.0.4-SNAPSHOT</yasson.version>
+ <yasson.version>3.0.5-SNAPSHOT</yasson.version>
<jakarta.json.bind.version>3.0.1</jakarta.json.bind.version>
<jakarta.json.version>2.1.3</jakarta.json.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
- <!-- TODO: Temporarily enable snapshot repository -->
- <!-- This can be removed once an official release of jakarta.json.bind-tck is available -->
- <repositories>
- <repository>
- <id>jakarta-snapshots</id>
- <url>https://jakarta.oss.sonatype.org/content/repositories/staging/</url>
- </repository>
- </repositories>
-
<dependencies>
<dependency>
<groupId>jakarta.json.bind</groupId>