test: serialization of subtype collections without properties Co-authored-by-AI: IBM Bob (1.126.0+bob2.2.0)
diff --git a/src/test/java/org/eclipse/yasson/customization/polymorphism/SubtypeCollectionTest.java b/src/test/java/org/eclipse/yasson/customization/polymorphism/SubtypeCollectionTest.java new file mode 100644 index 0000000..8e64439 --- /dev/null +++ b/src/test/java/org/eclipse/yasson/customization/polymorphism/SubtypeCollectionTest.java
@@ -0,0 +1,157 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * 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.customization.polymorphism; + +import java.util.List; +import java.util.Set; + +import jakarta.json.bind.annotation.JsonbCreator; +import jakarta.json.bind.annotation.JsonbSubtype; +import jakarta.json.bind.annotation.JsonbTypeInfo; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +import static org.eclipse.yasson.Jsonbs.defaultJsonb; + +/** + * Regression tests for https://github.com/eclipse-ee4j/yasson/issues/775 + * + * The root cause was a stale key left in the serialization context after the + * polymorphism discriminator lambda wrote its key+value pair directly to the + * generator (bypassing KeyWriter and its context.setKey(null) cleanup). + * + * The stale key caused the outer KeyWriter — wrapping the per-element object + * serializer chain — to call generator.writeKey() while the generator was in + * IN_ARRAY state when processing any element after the first. + * + * Three conditions are required to trigger the bug: + * 1. A type annotated with @JsonbTypeInfo (adds the raw discriminator lambda). + * 2. The subtype has NO other serializable properties (so no subsequent + * KeyWriter-wrapped property serializer clears the stale key). + * 3. More than one instance appears consecutively in a collection or array + * (so the outer KeyWriter fires again on the second element). + */ +public class SubtypeCollectionTest { + + // ========================================================================= + // Scenario 1 – sealed interface + record subtypes (original report, #775) + // ========================================================================= + + @JsonbTypeInfo(key = "type", value = { + @JsonbSubtype(alias = "optionA", type = OptionA.class), + @JsonbSubtype(alias = "optionB", type = OptionB.class), + }) + public sealed interface TestJsonbInterface permits OptionA, OptionB { + } + + public record OptionA() implements TestJsonbInterface { + @JsonbCreator + public OptionA { + } + } + + public record OptionB() implements TestJsonbInterface { + @JsonbCreator + public OptionB { + } + } + + public record TestJsonbSetRecord(Set<TestJsonbInterface> interfaceSet) { + @JsonbCreator + public TestJsonbSetRecord { + } + } + + public record TestJsonbListRecord(List<TestJsonbInterface> interfaceList) { + @JsonbCreator + public TestJsonbListRecord { + } + } + + @Test + public void testSingleElementSetSerialization() { + String json = defaultJsonb.toJson(new TestJsonbSetRecord(Set.of(new OptionA()))); + assertThat(json, containsString("\"type\":\"optionA\"")); + } + + /** Reproduces issue #775. */ + @Test + public void testMultipleElementSetSerialization() { + String json = defaultJsonb.toJson(new TestJsonbSetRecord(Set.of(new OptionA(), new OptionB()))); + assertThat(json, containsString("\"type\":\"optionA\"")); + assertThat(json, containsString("\"type\":\"optionB\"")); + } + + @Test + public void testMultipleElementListSerialization() { + String json = defaultJsonb.toJson(new TestJsonbListRecord(List.of(new OptionA(), new OptionB(), new OptionA()))); + assertThat(json, is("{\"interfaceList\":[{\"type\":\"optionA\"},{\"type\":\"optionB\"},{\"type\":\"optionA\"}]}")); + } + + // ========================================================================= + // Scenario 2 – plain classes, interface with @JsonbTypeInfo + // ========================================================================= + + @JsonbTypeInfo(key = "kind", value = { + @JsonbSubtype(alias = "alpha", type = Alpha.class), + @JsonbSubtype(alias = "beta", type = Beta.class), + }) + public interface PlainInterface { + } + + /** Plain class – no fields, no record. */ + public static class Alpha implements PlainInterface { + } + + /** Plain class – no fields, no record. */ + public static class Beta implements PlainInterface { + } + + public static class PlainWrapper { + public List<PlainInterface> items; + + public PlainWrapper(List<PlainInterface> items) { + this.items = items; + } + } + + @Test + public void testMultipleElementListPlainClassSerialization() { + String json = defaultJsonb.toJson(new PlainWrapper(List.of(new Alpha(), new Beta(), new Alpha()))); + assertThat(json, is("{\"items\":[{\"kind\":\"alpha\"},{\"kind\":\"beta\"},{\"kind\":\"alpha\"}]}")); + } + + // ========================================================================= + // Scenario 3 – array instead of collection + // ========================================================================= + + @Test + public void testMultipleElementArraySerialization() { + PlainInterface[] arr = { new Alpha(), new Beta(), new Alpha() }; + String json = defaultJsonb.toJson(arr); + assertThat(json, is("[{\"kind\":\"alpha\"},{\"kind\":\"beta\"},{\"kind\":\"alpha\"}]")); + } + + // ========================================================================= + // Scenario 4 – repeated same subtype in a collection + // ========================================================================= + + @Test + public void testRepeatedSameSubtypeInListSerialization() { + String json = defaultJsonb.toJson(new PlainWrapper(List.of(new Alpha(), new Alpha()))); + assertThat(json, is("{\"items\":[{\"kind\":\"alpha\"},{\"kind\":\"alpha\"}]}")); + } +}