Merge pull request #710 from KyleAure/707-fix-isIntegralNumber
707 fix is integral number
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 56e8e7c..6374c2a 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -24,20 +24,26 @@
jobs:
build:
- name: Test on JDK ${{ matrix.java_version }}
+ name: Test on JDK ${{ matrix.java_version }} locale ${{ matrix.locale }}
runs-on: ubuntu-latest
strategy:
matrix:
java_version: [ 11, 17, 21 ]
+ include:
+ - locale: en_US
+ # Use a different locale when running against one of the java versions
+ # to ensure we correctly write tests that account for localization differences.
+ - locale: de_AT
+ java_version: 21
steps:
- name: Checkout for build
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Set up compile JDK
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5.6.0
with: #Compile java needs to be the highest to ensure proper compilation of the multi-release jar
distribution: 'temurin'
java-version: 17
@@ -49,6 +55,12 @@
- name: Yasson install
run: mvn -U -C clean install -DskipTests
- name: Yasson tests
- run: mvn -U -B -C -Dmaven.javadoc.skip=true verify
+ run: |
+ export LANG_TAG="${{ matrix.locale }}"
+ mvn -U -B -C \
+ -Dmaven.javadoc.skip=true \
+ -Duser.language="${LANG_TAG%%_*}" \
+ -Duser.country="${LANG_TAG##*_}" \
+ verify
- name: JSONB-API TCK
run: cd yasson-tck && mvn -U -B test -DargLine="-Djava.locale.providers=COMPAT"
diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml
new file mode 100644
index 0000000..1e06af1
--- /dev/null
+++ b/.github/workflows/performance.yml
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2026 Eclipse Foundation 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
+#
+
+# This workflow performs performance testing using JMH.
+# It is intended to be run on a schedule (e.g. weekly) to track performance over time, but can also be triggered manually when needed.
+# Persist results to github pages for continuous line graphs and regression alerts.
+
+name: Performance
+
+on:
+ schedule:
+ # Runs every Tuesday at 4:13 AM UTC (avoid peak hours for better performance results)
+ - cron: '13 4 * * 2'
+ workflow_dispatch:
+
+jobs:
+ performance:
+ name: Run JMH performance tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout for performance
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ - name: Set up compile JDK
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with: #Compile java needs to be the highest to ensure proper compilation of the multi-release jar
+ distribution: 'temurin'
+ java-version: 17
+ cache: 'maven'
+ - name: Compile Yasson
+ run: |
+ mvn -U -C clean install -DskipTests
+ - name: Run JMH performance tests
+ run: |
+ cd yasson-jmh
+ mvn -U -B clean package
+ java -jar target/yasson-jmh.jar -rf json -rff jmh-result.json
+ - name: Persist JMH results
+ uses: benchmark-action/github-action-benchmark@52576c92bccf6ac60c8223ec7eb2565637cae9ba # v1.22.1
+ with:
+ name: Java JMH Benchmark
+ tool: 'jmh'
+ output-file-path: jmh-result.json
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ # Enable deployment to GitHub Pages for continuous line graphs
+ auto-push: true
+ comment-on-alert: true # Creates a comment if performance degrades
+ fail-on-alert: true # Fails the workflow run on regression
+ alert-threshold: '200%' # Triggers an alert if execution time doubles
+
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
new file mode 100644
index 0000000..5329115
--- /dev/null
+++ b/.github/workflows/verify.yml
@@ -0,0 +1,54 @@
+#
+# Copyright (c) 2026 Eclipse Foundation 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
+#
+
+# This workflow is used to verify this project against staged versions of the API and TCK.
+# It is not intended to be used for regular CI builds, but rather as a manual workflow that can be triggered when needed.
+
+name: Verify
+
+on:
+ workflow_dispatch:
+ inputs:
+ jsonb_version:
+ description: 'The version of the TCK to verify against (e.g. 3.1.0-M1)'
+ required: true
+
+jobs:
+ verify:
+ name: Verify against staged API or TCK
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout for verify
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 0
+ - name: Set up compile JDK
+ uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
+ with: #Compile java needs to be the highest to ensure proper compilation of the multi-release jar
+ distribution: 'temurin'
+ java-version: 17
+ cache: 'maven'
+ - name: Compile Yasson
+ run: |
+ mvn -U -C clean install \
+ -Pstaged \
+ -Djakarta.json.bind.version=${{ github.event.inputs.jsonb_version }} \
+ -DskipTests
+ - name: Run TCK
+ run: |
+ cd yasson-tck
+ mvn -U -B test \
+ -Pstaged \
+ -Djakarta.json.bind.version=${{ github.event.inputs.jsonb_version }} \
+ -Djsonb.tck.version=${{ github.event.inputs.jsonb_version }} \
+ -DargLine="-Djava.locale.providers=COMPAT"
diff --git a/pom.xml b/pom.xml
index 0a0c8a3..fcbef73 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
<parent>
<groupId>org.eclipse.ee4j</groupId>
<artifactId>project</artifactId>
- <version>2.0.2</version>
+ <version>2.0.4</version>
</parent>
<groupId>org.eclipse</groupId>
@@ -34,21 +34,30 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
<maven.compiler.release>11</maven.compiler.release>
+ <maven.compiler.target>11</maven.compiler.target>
<maven.compiler.testRelease>${maven.compiler.release}</maven.compiler.testRelease>
+
<nexus.staging.repository>yasson-maven2-staging</nexus.staging.repository>
<!--Dependencies-->
<hamcrest.version>2.2</hamcrest.version>
+
+ <!-- Jakarta API -->
<jakarta.annotation-api.version>3.0.0</jakarta.annotation-api.version>
<jakarta.el-api.version>6.0.0</jakarta.el-api.version>
<jakarta.enterprise.cdi-api.version>4.1.0</jakarta.enterprise.cdi-api.version>
<jakarta.interceptor-api.version>2.2.0</jakarta.interceptor-api.version>
<jakarta.json.bind.version>3.0.1</jakarta.json.bind.version>
<jakarta.json.version>2.1.3</jakarta.json.version>
- <jakarta.parson.version>1.1.7</jakarta.parson.version>
- <junit-jupiter.version>5.10.2</junit-jupiter.version>
- <weld-se-core.version>6.0.0.Beta1</weld-se-core.version>
+
+ <!-- Jakarta Implementation -->
+ <jakarta.parson.version>1.1.9</jakarta.parson.version>
+
+ <!-- Test dependencies-->
+ <junit-jupiter.version>5.14.4</junit-jupiter.version> <!-- TODO update to Junit 6 when running on Java 17+ -->
+ <weld-se-core.version>6.0.4.Final</weld-se-core.version>
<!--Plugins-->
<build-helper-maven-plugin.version>3.6.0</build-helper-maven-plugin.version>
@@ -365,9 +374,9 @@
<goal>compile</goal>
</goals>
<configuration>
- <release>11</release>
- <source>11</source>
- <target>11</target>
+ <release>${maven.compiler.release}</release>
+ <source>${maven.compiler.release}</source>
+ <target>${maven.compiler.release}</target>
</configuration>
</execution>
<execution>
@@ -434,7 +443,7 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>${maven-javadoc-plugin.version}</version>
<configuration>
- <bottom><![CDATA[Copyright © 2017, 2024 Oracle Corporation. All rights reserved.<br>]]></bottom>
+ <bottom><![CDATA[Copyright © 2017, 2026 Oracle Corporation. All rights reserved.<br>]]></bottom>
</configuration>
</plugin>
<plugin>
diff --git a/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java b/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java
index b4a2cf8..2763ea9 100644
--- a/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java
+++ b/src/main/java/org/eclipse/yasson/internal/ComponentMatcher.java
@@ -24,6 +24,7 @@
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.adapter.JsonbAdapter;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
import jakarta.json.bind.serializer.JsonbDeserializer;
import jakarta.json.bind.serializer.JsonbSerializer;
@@ -142,9 +143,28 @@
ComponentBoundCustomization customization) {
if (customization == null || customization.getSerializerBinding() == null) {
- return searchComponentBinding(propertyRuntimeType, ComponentBindings::getSerializer);
+ return searchComponentBinding(propertyRuntimeType, ComponentBindings::getSerializer, this::getAnnotationBasedSerializer);
}
- return Optional.of(customization.getSerializerBinding());
+ final SerializerBinding<?> binding = customization.getSerializerBinding();
+
+ // If the binding type exactly matches the runtime type, use it (optimization)
+ if (binding.getBindingType().equals(propertyRuntimeType)) {
+ return Optional.of(binding);
+ }
+
+ // Special handling for Object type: search for more specific serializers based on runtime type
+ // This allows annotation-based or config-based serializers on concrete types to be found
+ // when the property is declared as Object but has a specific runtime type
+ if (Object.class.equals(binding.getBindingType())) {
+ final Optional<SerializerBinding<?>> moreSpecific = searchComponentBinding(propertyRuntimeType,
+ ComponentBindings::getSerializer, this::getAnnotationBasedSerializer);
+ if (moreSpecific.isPresent()) {
+ return moreSpecific;
+ }
+ }
+
+ // Use the customization binding (user explicitly configured it for this property)
+ return Optional.of(binding);
}
/**
@@ -175,7 +195,14 @@
if (customization == null || customization.getSerializeAdapterBinding() == null) {
return searchComponentBinding(propertyRuntimeType, ComponentBindings::getAdapterInfo);
}
- return Optional.of(customization.getSerializeAdapterBinding());
+ // Check if the customization's adapter binding matches the runtime type
+ AdapterBinding binding = customization.getSerializeAdapterBinding();
+ if (matches(propertyRuntimeType, binding.getBindingType())) {
+ return Optional.of(binding);
+ }
+ // The annotation-based adapter doesn't match the runtime type,
+ // fall through to search for a better match based on runtime type
+ return searchComponentBinding(propertyRuntimeType, ComponentBindings::getAdapterInfo);
}
/**
@@ -194,7 +221,19 @@
return Optional.of(customization.getDeserializeAdapterBinding());
}
- private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(Type runtimeType, Function<ComponentBindings, T> supplier) {
+ /**
+ * Search for a component binding for the given runtime type.
+ *
+ * @param runtimeType The runtime type to find a component for
+ * @param supplier Function to extract the desired component from ComponentBindings
+ * @param annotationDiscovery Optional function for runtime annotation discovery (null if not applicable)
+ * @param <T> The type of component binding to search for
+ * @return Optional containing the component binding if found
+ */
+ private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(
+ Type runtimeType,
+ Function<ComponentBindings, T> supplier,
+ Function<Class<?>, Optional<T>> annotationDiscovery) {
// First check if there is an exact match
ComponentBindings binding = userComponents.get(runtimeType);
if (binding != null) {
@@ -206,6 +245,15 @@
Optional<Class<?>> runtimeClass = ReflectionUtils.getOptionalRawType(runtimeType);
if (runtimeClass.isPresent()) {
+ // Check for annotation-based component on the runtime type itself
+ // Currently only used for @JsonbTypeSerializer during serialization
+ if (annotationDiscovery != null) {
+ Optional<T> annotationBased = annotationDiscovery.apply(runtimeClass.get());
+ if (annotationBased.isPresent()) {
+ return annotationBased;
+ }
+ }
+
// Check if any interfaces have a match
for (Class<?> ifc : runtimeClass.get().getInterfaces()) {
ComponentBindings ifcBinding = userComponents.get(ifc);
@@ -220,7 +268,7 @@
// check if the superclass has a match
Class<?> superClass = runtimeClass.get().getSuperclass();
if (superClass != null && superClass != Object.class) {
- Optional<T> superBinding = searchComponentBinding(superClass, supplier);
+ Optional<T> superBinding = searchComponentBinding(superClass, supplier, annotationDiscovery);
if (superBinding.isPresent()) {
return superBinding;
}
@@ -229,7 +277,64 @@
return Optional.empty();
}
-
+
+ // Convenience overload for components without annotation discovery (deserializers, adapters)
+ private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(
+ final Type runtimeType,
+ final Function<ComponentBindings, T> supplier) {
+ return searchComponentBinding(runtimeType, supplier, null);
+ }
+
+ /**
+ * Discovers and caches a serializer defined by @JsonbTypeSerializer annotation on a runtime type.
+ *
+ * <p>This method performs <strong>runtime</strong> annotation discovery during serialization,
+ * which is distinct from the build-time annotation introspection performed by AnnotationIntrospector.
+ * It is invoked when serializing a property where the runtime type is more specific than the
+ * declared type (e.g., a property declared as {@code Object} containing an instance of a class
+ * annotated with @JsonbTypeSerializer).</p>
+ *
+ * <p>Note: Only @JsonbTypeSerializer is checked, not @JsonbTypeAdapter or @JsonbTypeDeserializer,
+ * because:</p>
+ * <ul>
+ * <li>Serializers are unidirectional (serialization only), so runtime discovery is complete</li>
+ * <li>Deserializers don't apply - we lack runtime type information during deserialization</li>
+ * <li>Adapters are bidirectional - discovering them only at runtime during serialization
+ * would be incomplete since they couldn't be discovered during deserialization</li>
+ * </ul>
+ *
+ * @param clazz The runtime class to check for @JsonbTypeSerializer annotation
+ * @return SerializerBinding if annotation is present and successfully introspected, empty otherwise
+ */
+ private Optional<SerializerBinding<?>> getAnnotationBasedSerializer(final Class<?> clazz) {
+ // Check if the class has a @JsonbTypeSerializer annotation
+ final JsonbTypeSerializer annotation = clazz.getAnnotation(JsonbTypeSerializer.class);
+ if (annotation == null) {
+ return Optional.empty();
+ }
+
+ // Thread-safe get-or-create using compute
+ final SerializerBinding<?> binding = userComponents.compute(clazz, (type, bindings) -> {
+ // If already cached, return as-is
+ if (bindings != null && bindings.getSerializer() != null) {
+ return bindings;
+ }
+
+ // Create new serializer binding
+ final Class<? extends JsonbSerializer> serializerClass = annotation.value();
+ final JsonbSerializer<?> serializer = jsonbContext.getComponentInstanceCreator().getOrCreateComponent(serializerClass);
+ final SerializerBinding<?> newBinding = new SerializerBinding<>(clazz, serializer);
+
+ // Create or update ComponentBindings
+ if (bindings == null) {
+ return new ComponentBindings(clazz, newBinding, null, null);
+ }
+ return new ComponentBindings(clazz, newBinding, bindings.getDeserializer(), bindings.getAdapterInfo());
+ }).getSerializer();
+
+ return Optional.ofNullable(binding);
+ }
+
private <T> Optional<T> getMatchingBinding(Type runtimeType, ComponentBindings binding, Function<ComponentBindings, T> supplier) {
final T component = supplier.apply(binding);
if (component != null && matches(runtimeType, binding.getBindingType())) {
diff --git a/src/main/java/org/eclipse/yasson/internal/ReflectionUtils.java b/src/main/java/org/eclipse/yasson/internal/ReflectionUtils.java
index 47b5187..d83850b 100644
--- a/src/main/java/org/eclipse/yasson/internal/ReflectionUtils.java
+++ b/src/main/java/org/eclipse/yasson/internal/ReflectionUtils.java
@@ -252,7 +252,7 @@
}
}
- if (resolvedArgs[i] == null || resolvedArgs[i].equals(typeToResolve)) {
+ if (resolvedArgs[i] == null) {
if (typeToSearch instanceof Class) {
return Object.class;
}
@@ -262,6 +262,10 @@
typeToSearch));
}
}
+ // The expected type and the resolved type are the same, simply return the type
+ if (resolvedArgs[i].equals(typeToResolve)) {
+ return typeToResolve;
+ }
if (resolvedArgs[i] instanceof ParameterizedType) {
resolvedArgs[i] = resolveTypeArguments((ParameterizedType) resolvedArgs[i], typeToSearch);
} else if (unresolvedArg instanceof GenericArrayType) {
diff --git a/src/test/java/org/eclipse/yasson/defaultmapping/generics/GenericsTest.java b/src/test/java/org/eclipse/yasson/defaultmapping/generics/GenericsTest.java
index 42eb35a..907eb05 100644
--- a/src/test/java/org/eclipse/yasson/defaultmapping/generics/GenericsTest.java
+++ b/src/test/java/org/eclipse/yasson/defaultmapping/generics/GenericsTest.java
@@ -14,6 +14,7 @@
import static org.eclipse.yasson.Jsonbs.defaultJsonb;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertIterableEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -52,6 +53,7 @@
import org.eclipse.yasson.defaultmapping.generics.model.GenericArrayClass;
import org.eclipse.yasson.defaultmapping.generics.model.GenericTestClass;
import org.eclipse.yasson.defaultmapping.generics.model.GenericWithUnboundedWildcardClass;
+import org.eclipse.yasson.defaultmapping.generics.model.ListContainer;
import org.eclipse.yasson.defaultmapping.generics.model.LowerBoundTypeVariableWithCollectionAttributeClass;
import org.eclipse.yasson.defaultmapping.generics.model.MultiLevelExtendedGenericTestClass;
import org.eclipse.yasson.defaultmapping.generics.model.MultipleBoundsContainer;
@@ -516,14 +518,36 @@
}
@Test
- public void genericUpperBoundContainer() {
- final String expectedJson = "{\"tree\":{\"children\":[{\"children\":[],\"name\":\"child\"}],\"name\":\"parent\"}}";
+ public void genericUpperBoundContainer() throws Exception {
+ final String expectedJson = "{\"tree\":{\"children\":[{\"name\":\"child\"}],\"name\":\"parent\"}}";
final TreeContainer<TreeElement> container = new TreeContainer<>();
final TreeElement parent = new TreeElement("parent");
parent.setChildren(List.of(new TreeElement("child")));
container.setTree(parent);
- assertEquals(expectedJson, defaultJsonb.toJson(container));
+ // Use a new instance of Jsonb to avoid any caching
+ try (var jsonb = JsonbBuilder.create()) {
+ assertEquals(expectedJson, jsonb.toJson(container));
+ TreeContainer<TreeElement> result = jsonb.fromJson(expectedJson, new TestTypeToken<TreeContainer<TreeElement>>() {}.getType());
+ assertIterableEquals(container.getTree().getChildren(), result.getTree().getChildren());
+ }
+
+ }
+
+ @Test
+ public void genericUpperBoundContainerWithListContainer() throws Exception {
+ final String expectedJson = "{\"list\":[{\"children\":[{\"name\":\"child\"}],\"name\":\"parent\"}]}";
+ final ListContainer<TreeElement> container = new ListContainer<>();
+ final TreeElement parent = new TreeElement("parent");
+ parent.setChildren(List.of(new TreeElement("child")));
+ container.setList(List.of(parent));
+
+ // Use a new instance of Jsonb to avoid any caching
+ try (var jsonb = JsonbBuilder.create()) {
+ assertEquals(expectedJson, jsonb.toJson(container));
+ ListContainer<TreeElement> result = jsonb.fromJson(expectedJson, new TestTypeToken<ListContainer<TreeElement>>() {}.getType());
+ assertIterableEquals(container.getList(), result.getList());
+ }
}
diff --git a/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/ListContainer.java b/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/ListContainer.java
new file mode 100644
index 0000000..44b128f
--- /dev/null
+++ b/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/ListContainer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2025 IBM, Inc. and/or its affiliates.
+ *
+ * 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.defaultmapping.generics.model;
+
+import java.util.List;
+
+/**
+ *
+ * @author <a href="mailto:jperkins@ibm.com">James R. Perkins</a>
+ */
+public class ListContainer<T> {
+
+ private List<T> list;
+
+ public List<T> getList() {
+ return list;
+ }
+
+ public void setList(List<T> list) {
+ this.list = list;
+ }
+
+}
diff --git a/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeElement.java b/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeElement.java
index 8668761..a8dd238 100644
--- a/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeElement.java
+++ b/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeElement.java
@@ -12,16 +12,15 @@
package org.eclipse.yasson.defaultmapping.generics.model;
-import java.util.ArrayList;
-import java.util.List;
-
/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
-public class TreeElement implements TreeTypeContainer<TreeElement> {
+public class TreeElement extends TreeTypeContainer<TreeElement> {
- private final String name;
- private List<TreeElement> children = new ArrayList<>();
+ private String name;
+
+ public TreeElement() {
+ }
public TreeElement(final String name) {
this.name = name;
@@ -30,12 +29,25 @@
public String getName() {
return name;
}
-
- public List<TreeElement> getChildren() {
- return children;
- }
-
- public void setChildren(final List<TreeElement> children) {
- this.children = children;
- }
-}
+
+ public void setName(final String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ TreeElement that = (TreeElement) o;
+ return name != null ? name.equals(that.name) : that.name == null;
+ }
+}
diff --git a/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeTypeContainer.java b/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeTypeContainer.java
index 120ae73..f649f7c 100644
--- a/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeTypeContainer.java
+++ b/src/test/java/org/eclipse/yasson/defaultmapping/generics/model/TreeTypeContainer.java
@@ -17,9 +17,31 @@
/**
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
-public interface TreeTypeContainer<T extends TreeTypeContainer<T>> {
+public class TreeTypeContainer<T extends TreeTypeContainer<T>> {
+ private List<T> children;
- List<T> getChildren();
+ public List<T> getChildren() {
+ return children;
+ }
- void setChildren(List<T> children);
-}
+ public void setChildren(final List<T> children) {
+ this.children = children;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ TreeTypeContainer<?> that = (TreeTypeContainer<?>) o;
+ if (children == null) {
+ return that.children == null;
+ }
+
+ return children.containsAll(that.children) && that.children.containsAll(children);
+ }
+}
diff --git a/src/test/java/org/eclipse/yasson/documented/DocumentationExampleTest.java b/src/test/java/org/eclipse/yasson/documented/DocumentationExampleTest.java
index d49e4a0..5012e78 100644
--- a/src/test/java/org/eclipse/yasson/documented/DocumentationExampleTest.java
+++ b/src/test/java/org/eclipse/yasson/documented/DocumentationExampleTest.java
@@ -351,7 +351,7 @@
@JsonbDateFormat("dd.MM.yyyy")
public LocalDate birthDate;
- @JsonbNumberFormat("#0.00")
+ @JsonbNumberFormat(value = "#0.00", locale="en_US")
public BigDecimal salary;
}
@@ -375,24 +375,25 @@
public LocalDate birthDate;
+ @JsonbNumberFormat(value = "#0.00", locale="en_US") // TODO: remove if withNumberFormat is added to JsonbConfig builder
public BigDecimal salary;
}
- @Test
+ @Test //TODO https://github.com/eclipse-ee4j/yasson/issues/722
public void testDateNumberFormats2() {
Person10 p = new Person10();
p.name = "Jason Bourne";
p.birthDate = LocalDate.of(1999, 8, 7);
p.salary = new BigDecimal("123.45678");
Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()//
- .withDateFormat("dd.MM.yyyy", null)); // TODO: why no withNumberFormat?
+ .withDateFormat("dd.MM.yyyy", null)); // TODO: add withNumberFormat if added to JsonbConfig builder
String json = jsonb.toJson(p);
- assertEquals("{\"birthDate\":\"07.08.1999\",\"name\":\"Jason Bourne\",\"salary\":123.45678}", json);
+ assertEquals("{\"birthDate\":\"07.08.1999\",\"name\":\"Jason Bourne\",\"salary\":\"123.46\"}", json);
- Person9 after = jsonb.fromJson("{\"birthDate\":\"07.08.1999\",\"name\":\"Jason Bourne\",\"salary\":123.45678}", Person9.class);
+ Person9 after = jsonb.fromJson("{\"birthDate\":\"07.08.1999\",\"name\":\"Jason Bourne\",\"salary\":\"123.46\"}", Person9.class);
assertEquals(p.name, after.name);
assertEquals(p.birthDate, after.birthDate);
- assertEquals(p.salary, after.salary);
+ assertEquals(new BigDecimal("123.46"), after.salary);
}
public static class Customer {
diff --git a/src/test/java/org/eclipse/yasson/jsonstructure/Issue673.java b/src/test/java/org/eclipse/yasson/jsonstructure/Issue673.java
index 7aeca3a..be2a555 100644
--- a/src/test/java/org/eclipse/yasson/jsonstructure/Issue673.java
+++ b/src/test/java/org/eclipse/yasson/jsonstructure/Issue673.java
@@ -27,12 +27,35 @@
import java.util.Collections;
import java.util.stream.Collectors;
+/**
+ * Test case for Issue #673: Custom deserializers with polymorphic types and JSON structure API.
+ *
+ * <p>This test validates the interaction between:
+ * <ul>
+ * <li>Custom {@link JsonbDeserializer} implementations</li>
+ * <li>Polymorphic type handling via {@link JsonbTypeInfo} and {@link JsonbSubtype}</li>
+ * <li>JSON-P structure API ({@link JsonArray}, {@link JsonObject}, {@link JsonValue})</li>
+ * </ul>
+ *
+ * <p>The test ensures that custom deserializers can properly access and process JSON structure
+ * objects when deserializing complex types with polymorphic behavior.
+ *
+ * @see <a href="https://github.com/eclipse-ee4j/yasson/issues/673">Issue #673</a>
+ */
public class Issue673 {
+ /**
+ * Marker interface for objects that can be referenced.
+ * Implemented by both {@link Reference} and {@link IRIReference}.
+ */
public static interface Referenceable {
}
+ /**
+ * A reference object with a description field.
+ * Deserialized from JSON objects containing a "description" property.
+ */
public static class Reference implements Referenceable {
private String description;
@@ -46,6 +69,10 @@
}
}
+ /**
+ * An IRI (Internationalized Resource Identifier) reference.
+ * Deserialized from JSON string values representing URIs.
+ */
public static class IRIReference implements Referenceable {
private String value;
@@ -65,14 +92,27 @@
}
}
+ /**
+ * Interface for location types with polymorphic deserialization support.
+ * Uses {@link JsonbTypeInfo} to determine concrete type based on "type" field in JSON.
+ */
@JsonbTypeInfo(key = "type", value = {
- @JsonbSubtype(alias = Location.TYPE,
+ @JsonbSubtype(alias = Location.TYPE,
type = Location.class)
})
public static interface LocationInterface {
}
+ /**
+ * Concrete location implementation with custom deserializers for complex fields.
+ *
+ * <p>Demonstrates:
+ * <ul>
+ * <li>Array-to-string conversion via {@link TagsDeserializer}</li>
+ * <li>Polymorphic reference deserialization via {@link ReferenceableDeserializer}</li>
+ * </ul>
+ */
public static class Location implements LocationInterface {
public final static String TYPE = "Location";
@@ -80,6 +120,12 @@
private String tags;
private Referenceable referenceable;
+ /**
+ * Gets the tags as a comma-separated string.
+ * Uses custom deserializer to convert JSON array to string.
+ *
+ * @return comma-separated tag string
+ */
@JsonbTypeDeserializer(TagsDeserializer.class)
public String getTags() {
return tags;
@@ -89,6 +135,13 @@
this.tags = tags;
}
+ /**
+ * Gets the reference object.
+ * Uses custom deserializer to handle polymorphic deserialization
+ * from either string (IRI) or object (Reference) JSON values.
+ *
+ * @return the referenceable object
+ */
@JsonbTypeDeserializer(ReferenceableDeserializer.class)
public Referenceable getReference() {
return referenceable;
@@ -99,11 +152,20 @@
}
}
+ /**
+ * Custom deserializer that converts a JSON array of strings into a comma-separated string.
+ *
+ * <p>Example JSON: {@code ["tag1", "tag2", "tag3"]} → {@code "tag1, tag2, tag3"}
+ *
+ * <p>This tests the ability to use {@link JsonParser#getArray()} to access
+ * JSON structure objects during deserialization.
+ */
public static class TagsDeserializer implements JsonbDeserializer<String> {
@Override
public String deserialize(JsonParser jp, DeserializationContext dc, Type type) {
final JsonValue v = jp.getArray();
- if (v instanceof JsonArray arr) {
+ if (v instanceof JsonArray) {
+ JsonArray arr = (JsonArray) v;
return arr.stream()
.filter(JsonString.class::isInstance)
.map(JsonString.class::cast)
@@ -115,17 +177,35 @@
}
+ /**
+ * Custom deserializer that handles polymorphic deserialization of {@link Referenceable} objects.
+ *
+ * <p>Supports two JSON representations:
+ * <ul>
+ * <li>String value → {@link IRIReference} (e.g., {@code "http://example.com"})</li>
+ * <li>Object value → {@link Reference} (e.g., {@code {"description": "..."}})</li>
+ * </ul>
+ *
+ * <p>This tests the ability to:
+ * <ul>
+ * <li>Use {@link JsonParser#getValue()} to access JSON structure objects</li>
+ * <li>Recursively deserialize nested objects using {@link DeserializationContext#deserialize}</li>
+ * <li>Create new parsers from JSON-P structure objects</li>
+ * </ul>
+ */
public static class ReferenceableDeserializer implements JsonbDeserializer<Referenceable> {
@Override
public Referenceable deserialize(JsonParser jp, DeserializationContext dc, Type type) {
final JsonValue v = jp.getValue();
- if (v instanceof JsonString str) {
+ if (v instanceof JsonString) {
+ JsonString str = (JsonString) v;
return new IRIReference(str.getString());
}
- if (v instanceof JsonObject obj) {
+ if (v instanceof JsonObject) {
+ JsonObject obj = (JsonObject) v;
return dc.deserialize(Reference.class,
- Json.createParserFactory(Collections.EMPTY_MAP)
+ Json.createParserFactory(Collections.emptyMap())
.createParser(obj));
}
return null;
diff --git a/src/test/java/org/eclipse/yasson/jsonstructure/JsonStructureToParserAdapterTest.java b/src/test/java/org/eclipse/yasson/jsonstructure/JsonStructureToParserAdapterTest.java
index 37d4f15..8cf1779 100644
--- a/src/test/java/org/eclipse/yasson/jsonstructure/JsonStructureToParserAdapterTest.java
+++ b/src/test/java/org/eclipse/yasson/jsonstructure/JsonStructureToParserAdapterTest.java
@@ -285,7 +285,7 @@
Jsonb jsonb = JsonbBuilder.create();
Issue673.LocationInterface result = jsonb.fromJson(json, Issue673.LocationInterface.class);
-
+
assertNotNull(result);
assertTrue(result instanceof Issue673.Location);
Issue673.Location location = (Issue673.Location) result;
diff --git a/src/test/java/org/eclipse/yasson/serializers/SerializersTest.java b/src/test/java/org/eclipse/yasson/serializers/SerializersTest.java
index db0efdf..228ed1f 100644
--- a/src/test/java/org/eclipse/yasson/serializers/SerializersTest.java
+++ b/src/test/java/org/eclipse/yasson/serializers/SerializersTest.java
@@ -35,6 +35,7 @@
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.JsonbConfig;
import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
import jakarta.json.bind.config.PropertyOrderStrategy;
import jakarta.json.bind.serializer.DeserializationContext;
import jakarta.json.bind.serializer.JsonbDeserializer;
@@ -78,6 +79,7 @@
import static org.eclipse.yasson.Jsonbs.defaultJsonb;
import static org.eclipse.yasson.Jsonbs.nullableJsonb;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -812,4 +814,92 @@
}
+ /**
+ * Test that annotation-based serializers work when property is declared as Object
+ * but the runtime type has @JsonbTypeSerializer annotation.
+ * This is a regression test for issue #689.
+ */
+ @Test
+ public void testAnnotationBasedSerializerWithObjectTypedProperty() throws Exception {
+ try (Jsonb jsonb = JsonbBuilder.create()) {
+
+ final ObjectPropertyContainer container = new ObjectPropertyContainer();
+ final AnnotatedWithSerializerType objectInstance = new AnnotatedWithSerializerType();
+ objectInstance.value = "test";
+ container.annotatedAsObject = objectInstance;
+ container.annotatedConcrete = new AnnotatedWithSerializerType();
+ container.annotatedConcrete.value = "test2";
+
+ final String result = jsonb.toJson(container);
+
+ // Both properties should use the annotation-based serializer
+ final String expected = "{\"annotatedAsObject\":{\"valueField\":\"replaced value\"},\"annotatedConcrete\":{\"valueField\":\"replaced value\"}}";
+ assertEquals(expected, result);
+
+ // Deserialization: annotatedConcrete uses annotation-based deserializer
+ // annotatedAsObject is declared as Object so JSON-B creates a HashMap (expected behavior)
+ final ObjectPropertyContainer deserialized = jsonb.fromJson(expected, ObjectPropertyContainer.class);
+ // In the JSON, the type looks like an object and therefore is a map
+ assertInstanceOf(Map.class, deserialized.annotatedAsObject, "Object property deserializes to Map");
+ final Map<?, ?> map = (Map<?, ?>) deserialized.annotatedAsObject;
+ assertTrue(map.containsKey("valueField"));
+ assertEquals("replaced value", map.get("valueField"));
+ assertEquals("replaced value", deserialized.annotatedConcrete.value);
+ }
+ }
+
+ /**
+ * Test that field-level and method-level @JsonbTypeSerializer annotations work on Object-typed properties.
+ * This tests existing AnnotationIntrospector code (not runtime discovery).
+ */
+ @Test
+ public void testFieldAndMethodLevelSerializerOnObjectType() throws Exception {
+ try (Jsonb jsonb = JsonbBuilder.create()) {
+ final ObjectWithAnnotatedFields container = new ObjectWithAnnotatedFields();
+ container.fieldAnnotated = "test field";
+ container.setMethodAnnotated("test method");
+
+ final String result = jsonb.toJson(container);
+
+ // Both should use their respective serializers
+ final String expected = "{\"fieldAnnotated\":\"FIELD:test field\",\"methodAnnotated\":\"METHOD:test method\"}";
+ assertEquals(expected, result);
+ }
+ }
+
+ public static class ObjectWithAnnotatedFields {
+ @JsonbTypeSerializer(ObjectFieldSerializer.class)
+ public Object fieldAnnotated;
+
+ private Object methodAnnotated;
+
+ @JsonbTypeSerializer(ObjectMethodSerializer.class)
+ public Object getMethodAnnotated() {
+ return methodAnnotated;
+ }
+
+ public void setMethodAnnotated(Object methodAnnotated) {
+ this.methodAnnotated = methodAnnotated;
+ }
+ }
+
+ public static class ObjectFieldSerializer implements JsonbSerializer<Object> {
+ @Override
+ public void serialize(Object obj, JsonGenerator generator, SerializationContext ctx) {
+ generator.write("FIELD:" + obj.toString());
+ }
+ }
+
+ public static class ObjectMethodSerializer implements JsonbSerializer<Object> {
+ @Override
+ public void serialize(Object obj, JsonGenerator generator, SerializationContext ctx) {
+ generator.write("METHOD:" + obj.toString());
+ }
+ }
+
+ public static class ObjectPropertyContainer {
+ public Object annotatedAsObject; // Declared as Object - this was the bug scenario
+ public AnnotatedWithSerializerType annotatedConcrete; // Declared concretely - should always work
+ }
+
}
diff --git a/yasson-jmh/pom.xml b/yasson-jmh/pom.xml
index dcdd303..61d3046 100644
--- a/yasson-jmh/pom.xml
+++ b/yasson-jmh/pom.xml
@@ -5,6 +5,12 @@
<modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.eclipse.ee4j</groupId>
+ <artifactId>project</artifactId>
+ <version>2.0.4</version>
+ </parent>
+
<groupId>org.eclipse.yasson</groupId>
<artifactId>yasson-jmh</artifactId>
<version>1.0-SNAPSHOT</version>
@@ -14,7 +20,7 @@
<properties>
<jmh.version>1.21</jmh.version>
- <yasson.version>2.0.2-SNAPSHOT</yasson.version>
+ <yasson.version>3.0.5-SNAPSHOT</yasson.version>
</properties>
diff --git a/yasson-tck/pom.xml b/yasson-tck/pom.xml
index c3d36c7..bab505d 100644
--- a/yasson-tck/pom.xml
+++ b/yasson-tck/pom.xml
@@ -5,17 +5,38 @@
<modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.eclipse.ee4j</groupId>
+ <artifactId>project</artifactId>
+ <version>2.0.4</version>
+ <relativePath/>
+ </parent>
+
<groupId>org.eclipse</groupId>
<artifactId>yasson-tck</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
- <jsonb.tck.version>3.0.0</jsonb.tck.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>
+
+ <!-- API Versions -->
+ <jakarta.json.version>2.1.3</jakarta.json.version> <!-- EE10 -->
+ <jakarta.json.bind.version>3.0.1</jakarta.json.bind.version> <!-- EE10 -->
+
+ <!-- IMPL Versions -->
+ <jsonb.tck.version>3.0.0</jsonb.tck.version> <!-- EE10 -->
+ <yasson.version>3.0.5-SNAPSHOT</yasson.version> <!-- EE10 -->
+
+ <!-- Test Versions -->
+ <junit-jupiter.version>5.14.4</junit-jupiter.version> <!-- TODO update to Junit 6 when running on Java 17+ -->
+ <weld-se-core.version>6.0.4.Final</weld-se-core.version>
+ <arquillian-junit5-container.version>1.8.0.Final</arquillian-junit5-container.version>
+
+ <!-- Plugin Versions -->
+ <maven-dependency-plugin.version>3.6.1</maven-dependency-plugin.version>
+ <maven-surefire-plugin.version>3.2.5</maven-surefire-plugin.version>
+ <maven-surefire-report-plugin.version>3.2.5</maven-surefire-report-plugin.version>
</properties>
<dependencies>
@@ -46,13 +67,13 @@
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
- <version>6.0.0.Beta1</version>
+ <version>${weld-se-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit5</groupId>
<artifactId>arquillian-junit5-container</artifactId>
- <version>1.8.0.Final</version>
+ <version>${arquillian-junit5-container.version}</version>
</dependency>
</dependencies>
@@ -61,7 +82,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
- <version>3.6.1</version>
+ <version>${maven-dependency-plugin.version}</version>
<executions>
<execution>
<id>copy</id>
@@ -96,7 +117,7 @@
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
- <version>3.2.5</version>
+ <version>${maven-surefire-plugin.version}</version>
<configuration>
<trimStackTrace>false</trimStackTrace>
<failIfNoTests>true</failIfNoTests>
@@ -111,7 +132,7 @@
</plugin>
<plugin>
<artifactId>maven-surefire-report-plugin</artifactId>
- <version>3.2.5</version>
+ <version>${maven-surefire-report-plugin.version}</version>
<executions>
<execution>
<id>post-unit-test</id>