2

I am currently working on a Spring Boot application (version 3.0.6) and using Spring Cloud (version 2022.0.2). I have two different endpoints ("/mvc" and "/message") that use two different request objects (DummyMessage1 and DummyMessage2), and I'm having some issues with Jackson's JsonProperty and JsonIgnoreProperties annotations.

In both the cases I'm looking to supress numbers from response and get tokens in response.

I have used JsonIgnoreProperties to suppress numbers from response.

@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)

In the /mvc endpoint, everything is working as expected. I'm using DummyMessage1 request object for this endpoint. The numbers field is suppressed from response as expected. Furthermore, the tokens field, is returned successfully in the JSON response and is not empty as it contains the list of tokens from the request.

The problem arises in the /message endpoint, which uses DummyMessage2 to post a message to kafka. DummyMessage2 is identical to DummyMessage1.

When I attempted to post a JSON to the Kafka topic, an exception was thrown:

Full stacktrace can be found here

java.lang.ClassCastException: class [B cannot be cast to class com.example.marshaller.model.DummyMessage2 ([B is in module java.base of loader 'bootstrap'; com.example.marshaller.model.DummyMessage2 is in unnamed module of loader 'app')
    at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.invokeConsumer(SimpleFunctionRegistry.java:990) ~[spring-cloud-function-context-4.0.2.jar:4.0.2]

Here are the request objects:

DummyMessage1:

@EqualsAndHashCode(callSuper = false)
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
public class DummyMessage1 extends BaseRequest {

    private String numbers;

    //@JsonProperty(access = JsonProperty.Access.READ_ONLY)
    public List<String> getTokens() {
        if (StringUtils.isBlank(this.numbers)) return Collections.emptyList();
        return List.of(this.numbers.split(";\\s*"));
    }
}

DummyMessage2:

@EqualsAndHashCode(callSuper = false)
@Getter
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
public class DummyMessage2 extends BaseRequest {

    private String numbers;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    public List<String> getTokens() {
        if (StringUtils.isBlank(this.numbers)) return Collections.emptyList();
        return List.of(this.numbers.split(";\\s*"));
    }
}

Next, to work around the the exception, I added the annotation @JsonProperty(access = JsonProperty.Access.READ_ONLY) on getTokens() method in DummyMessage2. However, this did not result in the desired outcome. The JSON posted to Kafka now did not include the numbers field as required, but also, the tokens field was as empty, which is not expected.

Why is this happening? I would expect the tokens field to be populated as it is in the /mvc endpoint. Any help or insights would be greatly appreciated.

Here's repository to reproduce the issue: https://github.com/cricketbackground/marshaller

Please note for security reasons the kafka brokers and kafka zk nodes are purposefully not set in the repo.

Request Body:


{
    "numbers":"12345; 3982934823; 3248923492834; 324923434"
}

How to use: please see here

--- Update

Here's the simple working DummyMessage2 by adding a field for the utility getter. In the getter method the field itself is returned if it is not empty, thus retaining the field value across multiple message post hops


    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import lombok.AllArgsConstructor;
    import lombok.EqualsAndHashCode;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.experimental.SuperBuilder;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.util.CollectionUtils;
    
    import java.util.Collections;
    import java.util.List;
    
    @EqualsAndHashCode(callSuper = false)
    @Getter
    @SuperBuilder
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonIgnoreProperties(ignoreUnknown = true, value = {"numbers"}, allowSetters = true)
    public class DummyMessage2 extends BaseRequest {
    
        private String numbers;
    
        private List<String> tokens;
    
        public List<String> getTokens() {
            if (!CollectionUtils.isEmpty(this.tokens)) return this.tokens;
            if (StringUtils.isBlank(this.numbers)) return Collections.emptyList();
            return List.of(this.numbers.split(";\\s*"));
        }
    }

The other solution suggested by Yevhenii Semenov also works but requires setter method logic to be written exactly reverse of getter logic. Setter method complexity increases based on getter method. Also, if there are more fields then writing/maintaining setter methods for each field becomes a problem

Vadiraj Purohit
  • 898
  • 8
  • 20

1 Answers1

2

The reason for such behavior is in your DummyMessage configuration. You have the String numbers field but ignore it for Jackson. Instead, you provide the getToken method. So Jackson thinks that your object has only the List<String> tokens field and doesn't understand your bytes when you unmarshal it.

Let's look closer:

When you call your /mvc endpoint, there is only marshaling happening, so it works. You have the following flow:

Request body (1)-> DummyMessage1 (2)-> Response body

{
  "numbers": "6469344427; 2017586291"
}

(1) ====> 

DummyMessage1(
  String numbers = "6469344427; 2017586291"
)

(2) ====> 

{
  "tokens": [
    "6469344427",
    "2017586291"
  ]
}

But in your /message endpoint, the flow is longer:

Request body (1)-> DummyMessage2 (2)-> Json (3)-> DummyMessage2

And on step 3, at your DummyMessageConsumerService, spring tries to unmarshal DummyMessage2 from JSON and can't do this because the Json has only the field "tokens," but there is no such field in the object.

To fix this issue:

  1. Remove @JsonProperty(access = JsonProperty.Access.READ_ONLY) from getTokens() method

  2. Add the following setter to your DummyMessage2

public void setTokens(List<String> tokens) {
    this.numbers = String.join(";", tokens);
}

Or you can create another DTO object like DummyMessage3 with List<String> tokens instead of String numbers. In that case the flow will be (1) -> DummyMessage2 (2)-> Json (3)-> DummyMessage3.

Yevhenii Semenov
  • 1,587
  • 8
  • 18
  • 1
    Please don't forget to upvote this post if I helped you – Yevhenii Semenov May 18 '23 at 21:31
  • I tried solutions you suggested. But it is not working. Is it possible can you share a working repository or send a pull request using my repository – Vadiraj Purohit May 20 '23 at 10:01
  • 1
    @Abc.Xyz I've posted patch here: https://gist.github.com/yevheniisemenov/fee586998f608e188875465108588d60 With this patch I can see the message `Received dummy message = DummyMessage2(numbers=6469344427; 2017586291)` in logs and also json `{"tokens":["6469344427","2017586291"]}` that posted to kafka topic `this.is.output.example.topic` Pls let me know is it work for you? – Yevhenii Semenov May 20 '23 at 21:42
  • 1
    Works as expected. Thanks for your detailed answer! – Vadiraj Purohit May 22 '23 at 09:21
  • I have updated OP with another simple solution. Please see update – Vadiraj Purohit May 22 '23 at 09:45