Explain type-id header mapping (setTypeIdPropertyName / setTypeIdMappings) and how it enables polymorphic JSON payloads over JMS.
answer
- type-id lives in a JMS string property, not the JSON body
- setTypeIdPropertyName names the property
- Default id = FQCN → coupling + risk
- setTypeIdMappings: logical id ↔ Class
- Same property name + mappings on both ends
basics
~20 sOn send, MappingJackson2MessageConverter writes an identifier for the payload class into a JMS string property named by setTypeIdPropertyName. On receive it reads that id to choose the class to deserialize into. setTypeIdMappings maps short logical ids to classes so different payload types can share one queue.
solid answer
~40 sJSON is schemaless, so on receive the converter must be told which Java class to deserialize into. MappingJackson2MessageConverter solves this with a type-id header: setTypeIdPropertyName("_type") names a JMS string property that toMessage stamps with the payload's type id and fromMessage reads back. By default the id is the fully-qualified class name, which couples both ends and forces identical packages. setTypeIdMappings(Map<String,Class<?>>) instead maps stable logical ids ("orderCreated" → OrderCreated.class) to classes, so a single destination can carry multiple, polymorphic payload types and each is resolved to the right concrete class. This decouples the wire contract from your package layout, survives refactors, enables cross-language producers, and acts as a whitelist that limits which classes can be instantiated — important because arbitrary type resolution is a deserialization risk. The same mappings must exist on both sides.
code
java · 23 lines@Bean
MessageConverter jacksonJmsMessageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type"); // JMS string property carrying the id
// Stable logical ids decouple the wire from package/class names and whitelist allowed types
Map<String, Class<?>> typeIdMappings = new HashMap<>();
typeIdMappings.put("orderCreated", OrderCreated.class);
typeIdMappings.put("orderCancelled", OrderCancelled.class);
converter.setTypeIdMappings(typeIdMappings);
return converter;
}
// Polymorphic consumption: one method per kind using a JMS selector on the type-id property
@Component
class OrderListeners {
@JmsListener(destination = "orders", selector = "_type = 'orderCancelled'")
void onCancel(OrderCancelled event) { /* ... */ }
@JmsListener(destination = "orders", selector = "_type = 'orderCreated'")
void onCreate(OrderCreated event) { /* ... */ }
}go deeper
Know the type-id tells the receiver which class to build.
Explain setTypeIdPropertyName on send/receive and that FQCN is the default id.
Use setTypeIdMappings for decoupled, polymorphic, whitelisted payloads and keep both sides in sync.
Govern logical ids as a versioned, cross-language contract and a deserialization security boundary.
## The core problem JSON on the wire carries no type information about *which Java class* it represents. When a message arrives, `MappingJackson2MessageConverter.fromMessage` must hand Jackson a concrete target type. It gets that type from a **type-id header** — a JMS message property. ## `setTypeIdPropertyName(String)` This names the JMS **string property** used to convey the type. For example `converter.setTypeIdPropertyName("_type")`. - **On send** (`toMessage`): after serializing the object to JSON, the converter computes a type id for the object's class and writes it as `message.setStringProperty("_type", <id>)`. - **On receive** (`fromMessage`): it reads `message.getStringProperty("_type")`, resolves it to a `Class`/`JavaType`, and asks Jackson to parse the JSON into that type. If the property name is unset or the property is missing, it throws `MessageConversionException`. ## Default id = fully-qualified class name Without explicit mappings, the type id *is* the FQCN, e.g. `com.acme.orders.OrderCreated`. Downsides: - **Tight coupling**: the consumer must have that exact class in that exact package. - **Refactor-fragile**: renaming/moving the class breaks in-flight and stored messages. - **Leaks internals + security**: the wire tells attackers your class names, and blindly resolving arbitrary FQCNs to instantiate is a deserialization hazard. ## `setTypeIdMappings(Map<String, Class<?>>)` Provide a bidirectional map from **stable logical ids** to concrete classes: ``` mappings.put("orderCreated", OrderCreated.class); mappings.put("orderCancelled", OrderCancelled.class); ``` - **Send**: the class is looked up in the map's inverse to emit the logical id (`orderCreated`) instead of the FQCN. - **Receive**: the logical id maps to the concrete class. Benefits: the wire contract is decoupled from package structure; producer and consumer can use *different* class names/packages as long as the logical ids agree; it whitelists exactly the allowed types (a safety boundary); and it enables **polymorphic payloads** — one queue carrying `orderCreated`, `orderCancelled`, `orderShipped`, each resolved to its own concrete type. ## Polymorphism in practice A listener can accept a common supertype/interface and branch on the concrete type, or you can have several `@JmsListener` methods with a JMS **message selector** on the type-id property (e.g. `selector = "_type = 'orderCancelled'"`) so each method only receives its kind. The converter's job is purely: id → concrete class → parsed object. ## Critical gotchas - **Both sides must share the same `typeIdPropertyName` AND the same `typeIdMappings`.** A logical id present on the producer but missing on the consumer fails to resolve → `MessageConversionException`. - **Selectors reference the property name you chose.** If you rename `_type`, update selectors too. - **This is Spring's JMS type-id, not Jackson's `@JsonTypeInfo`.** It lives in a JMS header, not embedded in the JSON body. Don't conflate the two mechanisms. - **Versioning.** Keep logical ids stable forever; add new ids for new types rather than repurposing old ones. - **Interop.** Non-Java producers just set the same string property value; they don't need the Java class — only the id contract.
- Why prefer setTypeIdMappings over the default fully-qualified class name id?It decouples the wire contract from your package layout (survives class renames/moves), lets producer and consumer use different class names, supports non-Java producers, and acts as a whitelist limiting which classes can be instantiated — reducing deserialization risk and information leakage.
- How is this type-id different from Jackson's @JsonTypeInfo polymorphic typing?Spring's JMS type-id is a JMS message header (a string property) read by MappingJackson2MessageConverter to pick the target class; @JsonTypeInfo embeds a discriminator inside the JSON body and is resolved by Jackson itself. They're independent mechanisms; the JMS one keeps the JSON body clean and is queryable via message selectors.
- What breaks if the producer and consumer disagree on the type-id mappings?fromMessage cannot resolve the incoming id to a class and throws MessageConversionException; the message typically ends up redelivered and eventually dead-lettered. The property name and the id→class map must match on both sides.
saying these in an interview costs you the question
- Believing the type-id is stored inside the JSON body (it's a JMS header property)
- Thinking FQCN ids are fine — they couple deployments and are refactor-fragile and a security concern
- Setting mappings on only one side and expecting resolution to work
- Confusing Spring's JMS type-id with Jackson @JsonTypeInfo