While looking into Azure Service bus with Spring Boot, I stumbled upon a nasty stacktrace caused by an IllegalArgumentException:

java.lang.IllegalArgumentException: Property name must not be null

So what is going on here?

Setup

I configured a queue persons on Azure service bus and in my spring boot application I defined a QueueListener using the @JmsListener annotation

@Component
public class PersonQueueListener {
    private static final String QUEUENAME="persons";

    @JmsListener(destination = QUEUENAME)
    public void receiveMessage(Person person) throws JsonProcessingException {
        log.info("Received Person {} on Service Bus", person);
    }
}

Somehow I expected this to work - after all the Person class was filled with Jackson annotations. However, exceptions were flying around with the only clue Property name must not be null.

Root cause

org.springframework.jms.listener.adapter.ListenerExecutionFailedException: Listener method 'public void com.example.demo.PersonQueueListener.receiveMessage(com.example.demo.Person) throws com.fasterxml.jackson.core.JsonProcessingException' threw exception; nested exception is java.lang.IllegalArgumentException: Property name must not be null
	at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:122) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:77) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:736) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:696) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:674) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:331) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:270) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1237) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.executeOngoingLoop(DefaultMessageListenerContainer.java:1227) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:1120) ~[spring-jms-5.3.20.jar:5.3.20]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
Caused by: java.lang.IllegalArgumentException: Property name must not be null
	at org.apache.qpid.jms.message.JmsMessagePropertySupport.checkPropertyNameIsValid(JmsMessagePropertySupport.java:57) ~[qpid-jms-client-0.53.0.jar:na]
	at org.apache.qpid.jms.message.JmsMessagePropertyIntercepter.getProperty(JmsMessagePropertyIntercepter.java:692) ~[qpid-jms-client-0.53.0.jar:na]
	at org.apache.qpid.jms.message.JmsMessage.getObjectProperty(JmsMessage.java:353) ~[qpid-jms-client-0.53.0.jar:na]
	at org.apache.qpid.jms.message.JmsMessage.getStringProperty(JmsMessage.java:393) ~[qpid-jms-client-0.53.0.jar:na]
	at org.springframework.jms.support.converter.MappingJackson2MessageConverter.getJavaTypeForMessage(MappingJackson2MessageConverter.java:453) ~[spring-jms-5.3.20.jar:5.3.20]
	at org.springframework.jms.support.converter.MappingJackson2MessageConverter.fromMessage(MappingJackson2MessageConverter.java:233) ~[spring-jms-5.3.20.jar:5.3.20]

Let’s take a side-step. When deserialising a JSON string with objectMapper, you explicitly provide the type to deserialise to. For instance:

String json="""
{
  "firstName": "Byram",
  "lastName": "Aguirrezabal"
}
""";
//creating a Person object from the json String
Person p = objectMapper.readValue(json, Person.class);

Now when receiving some text from a message queue, you don’t know what the type is: is it a Person, List, LocalDateTime, … and that is exactly what MappingJackson2MessageConverter.getJavaTypeForMessage is trying to figure out. According to the Javadocs:

Determine a Jackson JavaType for the given JMS Message, typically parsing a type id message property.

Solution

So you need to add a property to your JMS message stating ‘this message represents a Person, List, ….’, and you need to tell the MappingJackson2MessageConverter what the name of this property is with the setTypeIdPropertyName method - I lik the note in bold, that is hardly mentioned in any other documentation :)

Specify the name of the JMS message property that carries the type id for the contained object: either a mapped id value or a raw Java class name.

Default is none. NOTE: This property needs to be set in order to allow for converting from an incoming message to a Java object.

Now we are ready to define a custom bean:

 @Bean
public MessageConverter jsonJmsConverter() {
  MappingJackson2MessageConverter jackson = new MappingJackson2MessageConverter();
  //json is just text
  jackson.setTargetType(MessageType.TEXT);
  //each message should contain a property called _type and its value is the name of the class to serialize to
  jackson.setTypeIdPropertyName("_type");
  return jackson;
}

Allright, let’s test this out. Head over to the Azure portal, select your Service Bus instance and open the Service Bus Explorer (still in preview at the time of writing). In the explorer, you can send messages and put them on the bus.

Service Bus Explorer Things to note:

  1. the message body should contain valid JSON
  2. set the content type to application/json
  3. add a custom property so the converter can find out what class to deserialize to.

Restarting the Spring Boot app, and on the logs I now see this nice message without any stack traces.

2022-06-15 20:17:30.749  INFO 50892 --- [ntContainer#0-1] com.example.demo.PersonQueueListener     : Received Person com.example.demo.Person@7f445293 on Service Bus