2424
2525import jakarta .json .bind .JsonbConfig ;
2626import jakarta .json .bind .adapter .JsonbAdapter ;
27+ import jakarta .json .bind .annotation .JsonbTypeSerializer ;
2728import jakarta .json .bind .serializer .JsonbDeserializer ;
2829import jakarta .json .bind .serializer .JsonbSerializer ;
2930
@@ -142,9 +143,28 @@ public Optional<SerializerBinding<?>> getSerializerBinding(Type propertyRuntimeT
142143 ComponentBoundCustomization customization ) {
143144
144145 if (customization == null || customization .getSerializerBinding () == null ) {
145- return searchComponentBinding (propertyRuntimeType , ComponentBindings ::getSerializer );
146+ return searchComponentBinding (propertyRuntimeType , ComponentBindings ::getSerializer , this :: getAnnotationBasedSerializer );
146147 }
147- return Optional .of (customization .getSerializerBinding ());
148+ final SerializerBinding <?> binding = customization .getSerializerBinding ();
149+
150+ // If the binding type exactly matches the runtime type, use it (optimization)
151+ if (binding .getBindingType ().equals (propertyRuntimeType )) {
152+ return Optional .of (binding );
153+ }
154+
155+ // Special handling for Object type: search for more specific serializers based on runtime type
156+ // This allows annotation-based or config-based serializers on concrete types to be found
157+ // when the property is declared as Object but has a specific runtime type
158+ if (Object .class .equals (binding .getBindingType ())) {
159+ final Optional <SerializerBinding <?>> moreSpecific = searchComponentBinding (propertyRuntimeType ,
160+ ComponentBindings ::getSerializer , this ::getAnnotationBasedSerializer );
161+ if (moreSpecific .isPresent ()) {
162+ return moreSpecific ;
163+ }
164+ }
165+
166+ // Use the customization binding (user explicitly configured it for this property)
167+ return Optional .of (binding );
148168 }
149169
150170 /**
@@ -175,7 +195,14 @@ public Optional<AdapterBinding> getSerializeAdapterBinding(Type propertyRuntimeT
175195 if (customization == null || customization .getSerializeAdapterBinding () == null ) {
176196 return searchComponentBinding (propertyRuntimeType , ComponentBindings ::getAdapterInfo );
177197 }
178- return Optional .of (customization .getSerializeAdapterBinding ());
198+ // Check if the customization's adapter binding matches the runtime type
199+ AdapterBinding binding = customization .getSerializeAdapterBinding ();
200+ if (matches (propertyRuntimeType , binding .getBindingType ())) {
201+ return Optional .of (binding );
202+ }
203+ // The annotation-based adapter doesn't match the runtime type,
204+ // fall through to search for a better match based on runtime type
205+ return searchComponentBinding (propertyRuntimeType , ComponentBindings ::getAdapterInfo );
179206 }
180207
181208 /**
@@ -194,7 +221,19 @@ public Optional<AdapterBinding> getDeserializeAdapterBinding(Type propertyRuntim
194221 return Optional .of (customization .getDeserializeAdapterBinding ());
195222 }
196223
197- private <T extends AbstractComponentBinding > Optional <T > searchComponentBinding (Type runtimeType , Function <ComponentBindings , T > supplier ) {
224+ /**
225+ * Search for a component binding for the given runtime type.
226+ *
227+ * @param runtimeType The runtime type to find a component for
228+ * @param supplier Function to extract the desired component from ComponentBindings
229+ * @param annotationDiscovery Optional function for runtime annotation discovery (null if not applicable)
230+ * @param <T> The type of component binding to search for
231+ * @return Optional containing the component binding if found
232+ */
233+ private <T extends AbstractComponentBinding > Optional <T > searchComponentBinding (
234+ Type runtimeType ,
235+ Function <ComponentBindings , T > supplier ,
236+ Function <Class <?>, Optional <T >> annotationDiscovery ) {
198237 // First check if there is an exact match
199238 ComponentBindings binding = userComponents .get (runtimeType );
200239 if (binding != null ) {
@@ -206,6 +245,15 @@ private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(
206245
207246 Optional <Class <?>> runtimeClass = ReflectionUtils .getOptionalRawType (runtimeType );
208247 if (runtimeClass .isPresent ()) {
248+ // Check for annotation-based component on the runtime type itself
249+ // Currently only used for @JsonbTypeSerializer during serialization
250+ if (annotationDiscovery != null ) {
251+ Optional <T > annotationBased = annotationDiscovery .apply (runtimeClass .get ());
252+ if (annotationBased .isPresent ()) {
253+ return annotationBased ;
254+ }
255+ }
256+
209257 // Check if any interfaces have a match
210258 for (Class <?> ifc : runtimeClass .get ().getInterfaces ()) {
211259 ComponentBindings ifcBinding = userComponents .get (ifc );
@@ -220,7 +268,7 @@ private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(
220268 // check if the superclass has a match
221269 Class <?> superClass = runtimeClass .get ().getSuperclass ();
222270 if (superClass != null && superClass != Object .class ) {
223- Optional <T > superBinding = searchComponentBinding (superClass , supplier );
271+ Optional <T > superBinding = searchComponentBinding (superClass , supplier , annotationDiscovery );
224272 if (superBinding .isPresent ()) {
225273 return superBinding ;
226274 }
@@ -229,7 +277,64 @@ private <T extends AbstractComponentBinding> Optional<T> searchComponentBinding(
229277
230278 return Optional .empty ();
231279 }
232-
280+
281+ // Convenience overload for components without annotation discovery (deserializers, adapters)
282+ private <T extends AbstractComponentBinding > Optional <T > searchComponentBinding (
283+ final Type runtimeType ,
284+ final Function <ComponentBindings , T > supplier ) {
285+ return searchComponentBinding (runtimeType , supplier , null );
286+ }
287+
288+ /**
289+ * Discovers and caches a serializer defined by @JsonbTypeSerializer annotation on a runtime type.
290+ *
291+ * <p>This method performs <strong>runtime</strong> annotation discovery during serialization,
292+ * which is distinct from the build-time annotation introspection performed by AnnotationIntrospector.
293+ * It is invoked when serializing a property where the runtime type is more specific than the
294+ * declared type (e.g., a property declared as {@code Object} containing an instance of a class
295+ * annotated with @JsonbTypeSerializer).</p>
296+ *
297+ * <p>Note: Only @JsonbTypeSerializer is checked, not @JsonbTypeAdapter or @JsonbTypeDeserializer,
298+ * because:</p>
299+ * <ul>
300+ * <li>Serializers are unidirectional (serialization only), so runtime discovery is complete</li>
301+ * <li>Deserializers don't apply - we lack runtime type information during deserialization</li>
302+ * <li>Adapters are bidirectional - discovering them only at runtime during serialization
303+ * would be incomplete since they couldn't be discovered during deserialization</li>
304+ * </ul>
305+ *
306+ * @param clazz The runtime class to check for @JsonbTypeSerializer annotation
307+ * @return SerializerBinding if annotation is present and successfully introspected, empty otherwise
308+ */
309+ private Optional <SerializerBinding <?>> getAnnotationBasedSerializer (final Class <?> clazz ) {
310+ // Check if the class has a @JsonbTypeSerializer annotation
311+ final JsonbTypeSerializer annotation = clazz .getAnnotation (JsonbTypeSerializer .class );
312+ if (annotation == null ) {
313+ return Optional .empty ();
314+ }
315+
316+ // Thread-safe get-or-create using compute
317+ final SerializerBinding <?> binding = userComponents .compute (clazz , (type , bindings ) -> {
318+ // If already cached, return as-is
319+ if (bindings != null && bindings .getSerializer () != null ) {
320+ return bindings ;
321+ }
322+
323+ // Create new serializer binding
324+ final Class <? extends JsonbSerializer > serializerClass = annotation .value ();
325+ final JsonbSerializer <?> serializer = jsonbContext .getComponentInstanceCreator ().getOrCreateComponent (serializerClass );
326+ final SerializerBinding <?> newBinding = new SerializerBinding <>(clazz , serializer );
327+
328+ // Create or update ComponentBindings
329+ if (bindings == null ) {
330+ return new ComponentBindings (clazz , newBinding , null , null );
331+ }
332+ return new ComponentBindings (clazz , newBinding , bindings .getDeserializer (), bindings .getAdapterInfo ());
333+ }).getSerializer ();
334+
335+ return Optional .ofNullable (binding );
336+ }
337+
233338 private <T > Optional <T > getMatchingBinding (Type runtimeType , ComponentBindings binding , Function <ComponentBindings , T > supplier ) {
234339 final T component = supplier .apply (binding );
235340 if (component != null && matches (runtimeType , binding .getBindingType ())) {
0 commit comments