Merge pull request #777 from KyleAure/615-record-jsonbadapater

test: `@JsonbTypeInfo` name collision 
diff --git a/src/main/java/org/eclipse/yasson/internal/properties/MessageKeys.java b/src/main/java/org/eclipse/yasson/internal/properties/MessageKeys.java
index fde557a..2171f8b 100644
--- a/src/main/java/org/eclipse/yasson/internal/properties/MessageKeys.java
+++ b/src/main/java/org/eclipse/yasson/internal/properties/MessageKeys.java
@@ -285,6 +285,10 @@
      */
     PROPERTY_NAME_CLASH("propertyNameClash"),
     /**
+     * A {@code @JsonbTypeInfo} discriminator key collides with a property name on the implementing class.
+     */
+    TYPE_INFO_KEY_CLASH("typeInfoKeyClash"),
+    /**
      * {@link Date} is not supported I-Json is enabled.
      */
     SQL_DATE_IJSON_ERROR("sqlDateIJsonError"),
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java b/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java
index 9aa29a7..c7870b7 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/SerializationModelCreator.java
@@ -237,7 +237,10 @@
         }
         for (PropertyModel propertyModel : classModel.getSortedProperties()) {
             if (propertySerializers.containsKey(propertyModel.getWriteName())) {
-                throw new JsonbException("CHANGE naming conflict!");
+                throw new JsonbException(Messages.getMessage(MessageKeys.TYPE_INFO_KEY_CLASH,
+                        typeInheritanceConfiguration.getFieldName(),
+                        propertyModel.getWriteName(),
+                        classModel.getType().getName()));
             }
         }
     }
diff --git a/src/main/resources/yasson-messages.properties b/src/main/resources/yasson-messages.properties
index 244df20..59b7d23 100644
--- a/src/main/resources/yasson-messages.properties
+++ b/src/main/resources/yasson-messages.properties
@@ -81,6 +81,7 @@
 jsonbTransientWithOtherAnnotations=JsonbTransient annotation cannot be used with other jsonb annotations on the same property.
 nonParametrizedType=Type: {0} is not a parametrized type.
 propertyNameClash=Property {0} clashes with property {1} by read or write name in class {2}.
+typeInfoKeyClash=@JsonbTypeInfo discriminator key ''{0}'' conflicts with property ''{1}'' in class {2}. Rename the property or change the @JsonbTypeInfo key.
 sqlDateIJsonError=java.sql.Date is not supported in STRICT_IJSON mode.
 recursiveReference=Recursive reference has been found in {0}.
 datatypeFactoryCreationFailed=An error occurred while DatatypeFactory creation.
diff --git a/src/test/java/org/eclipse/yasson/customization/polymorphism/AnnotationPolymorphismTest.java b/src/test/java/org/eclipse/yasson/customization/polymorphism/AnnotationPolymorphismTest.java
index bf6ae1b..d5eb27a 100644
--- a/src/test/java/org/eclipse/yasson/customization/polymorphism/AnnotationPolymorphismTest.java
+++ b/src/test/java/org/eclipse/yasson/customization/polymorphism/AnnotationPolymorphismTest.java
@@ -22,6 +22,8 @@
 import jakarta.json.bind.annotation.JsonbTypeInfo;
 
 import org.eclipse.yasson.Jsonbs;
+import org.eclipse.yasson.internal.properties.MessageKeys;
+import org.eclipse.yasson.internal.properties.Messages;
 import org.junit.jupiter.api.Test;
 
 import static org.hamcrest.CoreMatchers.instanceOf;
@@ -145,4 +147,38 @@
 
     }
 
+    /**
+     * When a class has a field annotated {@code @JsonbProperty("reason")} and its
+     * declaring interface carries {@code @JsonbTypeInfo(key = "reason", ...)}, the
+     * type-discriminator key and the property name are identical.  Serialisation must
+     * throw a {@link JsonbException} rather than silently emit a duplicate JSON key.
+     */
+    @Test
+    public void testTypeInfoKeyClashWithPropertyNameThrowsOnSerialisation() {
+        ReasonEvent event = new ReasonEvent("some-value");
+        String expectedMessage = Messages.getMessage(MessageKeys.TYPE_INFO_KEY_CLASH,
+                "reason", "reason", ReasonEvent.class.getName());
+        JsonbException ex = assertThrows(JsonbException.class, () -> Jsonbs.defaultJsonb.toJson(event));
+        assertThat(ex.getMessage(), is(expectedMessage));
+    }
+
+    @JsonbTypeInfo(
+            key = "reason",
+            value = {
+                    @JsonbSubtype(alias = "event", type = ReasonEvent.class)
+            }
+    )
+    public interface ReasonHolder {
+    }
+
+    public static final class ReasonEvent implements ReasonHolder {
+
+        @JsonbProperty("reason")
+        public String reason;
+
+        public ReasonEvent(String reason) {
+            this.reason = reason;
+        }
+    }
+
 }
diff --git a/src/test/java/org/eclipse/yasson/records/NotificationModel.java b/src/test/java/org/eclipse/yasson/records/NotificationModel.java
new file mode 100644
index 0000000..ade9e02
--- /dev/null
+++ b/src/test/java/org/eclipse/yasson/records/NotificationModel.java
@@ -0,0 +1,111 @@
+/*
+ * 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.records;
+
+import jakarta.json.bind.adapter.JsonbAdapter;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeAdapter;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+
+/**
+ * Model types for the issue #615 reproducer.
+ * <p>
+ * The bug: when a sealed interface carries {@code @JsonbTypeInfo} and one of its
+ * abstract methods is annotated with {@code @JsonbTypeAdapter}, that adapter was
+ * silently ignored for implementing records, leaving the field {@code null} after
+ * deserialization.
+ */
+public final class NotificationModel {
+
+    private NotificationModel() {}
+
+    // ------------------------------------------------------------------
+    // Reason enum
+    // ------------------------------------------------------------------
+
+    public enum NotificationReason {
+        MENTION("mention"),
+        FOLLOW("follow");
+
+        private final String value;
+
+        NotificationReason(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public static NotificationReason fromString(String s) {
+            for (NotificationReason r : values()) {
+                if (r.value.equals(s)) {
+                    return r;
+                }
+            }
+            throw new IllegalArgumentException("Unknown reason: " + s);
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // Adapter
+    // ------------------------------------------------------------------
+
+    public static class NotificationReasonAdapter implements JsonbAdapter<NotificationReason, String> {
+
+        @Override
+        public String adaptToJson(NotificationReason obj) throws Exception {
+            return obj.getValue();
+        }
+
+        @Override
+        public NotificationReason adaptFromJson(String obj) throws Exception {
+            return NotificationReason.fromString(obj);
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // Sealed interface with @JsonbTypeInfo + @JsonbTypeAdapter on reason()
+    // ------------------------------------------------------------------
+
+    @JsonbTypeInfo(
+            key = "@reason",
+            value = {
+                    @JsonbSubtype(alias = "mention", type = MentionNotification.class),
+                    @JsonbSubtype(alias = "follow",  type = FollowNotification.class)
+            }
+    )
+    public sealed interface Notification permits MentionNotification, FollowNotification {
+
+        @JsonbTypeAdapter(NotificationReasonAdapter.class)
+        @JsonbProperty("reason")
+        NotificationReason reason();
+    }
+
+    // ------------------------------------------------------------------
+    // Record subtypes
+    // ------------------------------------------------------------------
+
+    public record MentionNotification(
+            @JsonbProperty("uri") String uri,
+            @JsonbTypeAdapter(NotificationReasonAdapter.class)
+            @JsonbProperty("reason") NotificationReason reason
+    ) implements Notification {}
+
+    public record FollowNotification(
+            @JsonbProperty("handle") String handle,
+            @JsonbTypeAdapter(NotificationReasonAdapter.class)
+            @JsonbProperty("reason") NotificationReason reason
+    ) implements Notification {}
+}
diff --git a/src/test/java/org/eclipse/yasson/records/RecordTest.java b/src/test/java/org/eclipse/yasson/records/RecordTest.java
index e8998b4..edaee35 100644
--- a/src/test/java/org/eclipse/yasson/records/RecordTest.java
+++ b/src/test/java/org/eclipse/yasson/records/RecordTest.java
@@ -14,6 +14,9 @@
 package org.eclipse.yasson.records;
 
 import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.annotation.JsonbProperty;
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
 
 import org.eclipse.yasson.Jsonbs;
 import org.eclipse.yasson.TestTypeToken;
@@ -267,4 +270,65 @@
         assertThat(result.items().get(1).required(), is("c"));
         assertThat(result.items().get(1).optional(), is((String) null));
     }
+
+    // -----------------------------------------------------------------
+    // Issue #615 — @JsonbTypeAdapter on record components with @JsonbTypeInfo
+    // -----------------------------------------------------------------
+
+    /**
+     * The original issue reported that {@code @JsonbTypeAdapter} on a record component was
+     * silently ignored during (de)serialisation when the declaring interface also carried
+     * {@code @JsonbTypeInfo}; that was resolved by
+     * https://github.com/eclipse-ee4j/yasson/pull/770
+     */
+    @Test
+    public void testNotificationMentionSerializesReasonViaAdapter() {
+        NotificationModel.MentionNotification mention =
+                new NotificationModel.MentionNotification("https://example.com/post/1",
+                                                          NotificationModel.NotificationReason.MENTION);
+        String json = Jsonbs.defaultJsonb.toJson(mention, NotificationModel.MentionNotification.class);
+        assertThat(json, is("{\"@reason\":\"mention\",\"reason\":\"mention\",\"uri\":\"https://example.com/post/1\"}"));
+    }
+
+    @Test
+    public void testNotificationMentionDeserializesReasonViaAdapter() {
+        String json = "{\"@reason\":\"mention\",\"reason\":\"mention\",\"uri\":\"https://example.com/post/1\"}";
+        NotificationModel.Notification notification =
+                Jsonbs.defaultJsonb.fromJson(json, NotificationModel.Notification.class);
+        assertThat(notification, instanceOf(NotificationModel.MentionNotification.class));
+        NotificationModel.MentionNotification mention = (NotificationModel.MentionNotification) notification;
+        assertThat(mention.uri(), is("https://example.com/post/1"));
+        assertThat(mention.reason(), is(NotificationModel.NotificationReason.MENTION));
+    }
+
+    /**
+     * Same clash as {@code testTypeInfoKeyClashWithPropertyNameThrowsOnSerialisation} in
+     * {@link org.eclipse.yasson.customization.polymorphism.AnnotationPolymorphismTest},
+     * but using a record component instead of a plain class field.
+     * <p>
+     * The {@code @JsonbTypeInfo(key = "reason")} discriminator and the record component
+     * {@code @JsonbProperty("reason")} map to the same JSON key.  Serialisation must
+     * throw a {@link JsonbException} with a descriptive message rather than silently
+     * emitting a duplicate key.
+     */
+    @Test
+    public void testRecordTypeInfoKeyClashWithComponentNameThrowsOnSerialisation() {
+        ReasonRecord record = new ReasonRecord("some-value");
+        String expectedMessage = Messages.getMessage(MessageKeys.TYPE_INFO_KEY_CLASH,
+                "reason", "reason", ReasonRecord.class.getName());
+        JsonbException ex = assertThrows(JsonbException.class, () -> Jsonbs.defaultJsonb.toJson(record));
+        assertThat(ex.getMessage(), is(expectedMessage));
+    }
+
+    @JsonbTypeInfo(
+            key = "reason",
+            value = {
+                    @JsonbSubtype(alias = "event", type = ReasonRecord.class)
+            }
+    )
+    public interface ReasonHolder {
+    }
+
+    public record ReasonRecord(@JsonbProperty("reason") String reason) implements ReasonHolder {
+    }
 }