4

I have a request coming like this

{
    "varA"  : "A",
    "varB" : "TCFNhbiBKb3NlMRgwFgYDVQQK"
}

where key varB is a base64 encoded JSON string. something like this:

{
    "nestedVarB1": "some value here",
    "nestedVarB2" : "some other value here"
}

I want to convert it into some POJOs like this:

@Data
public class MyRequest {
    private String varA;
    private B varB;
}

@Data
public class B {
    private String nestedVarB1;
    private String nestedVarB2;
}

I went through several solutions on stack overflow like this and this, but I want to convert the JSON directly into an object of type MyRequest maybe by writing some sort of Jackson deserializer.

How can I convert the JSON directly into MyRequest using Jackson and Spring Boot?

Note:

  1. POJOs MyRequest and B are very big and B could be further nested, so doing it manually would be a big task.
  2. I don't have any control over the request coming, it is coming from a third party source. So, can't change the format of the request.
Saheb
  • 1,392
  • 3
  • 13
  • 33
  • 1
    your are correct that you need to write your own deserializer and register it using the jackson annotation `@JsonDeserialize(using = foodeserilizer.class)`. – Amit Jun 05 '17 at 14:48
  • Can you suggest how should I write a custom deserializer which just decodes base64 encoded JSON and then use the default deserializer? I have found an answer [here](https://stackoverflow.com/questions/18313323/how-do-i-call-the-default-deserializer-from-a-custom-deserializer-in-jackson), but how should I ask the default deserializer to treat the decoded string as JSON for rest of the object? – Saheb Jun 05 '17 at 14:58

1 Answers1

6

I've done some experiments and here is a simple Jackson Deserializer which should work for you.

Deserializer implements the ContextualDeserializer interface to get access to actual bean property (for example varB). It's necessary for detecting correct result type, because deserializer itself can be attached to a field of any type.

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;

import java.io.IOException;
import java.util.Base64;

public class Base64Deserializer extends JsonDeserializer<Object> implements ContextualDeserializer {

    private Class<?> resultClass;

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext context, BeanProperty property) throws JsonMappingException {
        this.resultClass = property.getType().getRawClass();
        return this;
    }

    @Override
    public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
        String value = parser.getValueAsString();
        Base64.Decoder decoder = Base64.getDecoder();

        try {
            ObjectMapper objectMapper = new ObjectMapper();
            byte[] decodedValue = decoder.decode(value);

            return objectMapper.readValue(decodedValue, this.resultClass);
        } catch (IllegalArgumentException | JsonParseException e) {
            String fieldName = parser.getParsingContext().getCurrentName();
            Class<?> wrapperClass = parser.getParsingContext().getCurrentValue().getClass();

            throw new InvalidFormatException(
                parser,
                String.format("Value for '%s' is not a base64 encoded JSON", fieldName),
                value,
                wrapperClass
            );
        }
    }
}

Here is an example of mapped class.

public class MyRequest {

    private String varA;

    @JsonDeserialize(using = Base64Deserializer.class)
    private B varB;

    public String getVarA() {
        return varA;
    }

    public void setVarA(String varA) {
        this.varA = varA;
    }

    public B getVarB() {
        return varB;
    }

    public void setVarB(B varB) {
        this.varB = varB;
    }
}
Lukáš Kováč
  • 381
  • 1
  • 3
  • 5
  • I noticed that the request coming is in `application/x-www-form-urlencoded` format so I can't use Jackson serializer here. Accepting your answer as it works fine for Application/JSON asked in the question. Can you suggest something if data is coming `application/x-www-form-urlencoded`? – Saheb Jun 06 '17 at 07:51
  • Can you please provide an example of request? Keys "varA", "varB" are on the top level of the request, or they are wrapped? – Lukáš Kováč Jun 06 '17 at 16:26
  • yes `varA` and `varB` are top level. Something like this `varB={base64encoded string here}&varA="some string"`. Also, I have asked a separate question related to this [here](https://stackoverflow.com/questions/44384557/custom-deserializer-for-requests-with-content-type-application-x-www-form-urlenc). – Saheb Jun 06 '17 at 17:27
  • just to clarify, so top level object comes as `x-www-form-urlencoded`, with `varB` which contains base64 encoded JSON, correct? – Lukáš Kováč Jun 06 '17 at 17:34
  • This happens for real: https://developer.android.com/google/play/billing/rtdn-reference so thanks for this answer – kris larson Oct 07 '21 at 14:31