djrollins
djrollins

Reputation: 300

Simplest way to decode and deserialize field whose value is a base64 encoded, stringified JSON blob with Jackson

In our spring data projects we have "standard" approach to writing our DTOs, where we use lombok's @Value and @Builder for immutability and @JsonDeserialize(builder = SomeClass.SomeClassBuilder.class) for jackson deserialization.

Here is a minimal example:

@RestController
class Controller {
    @PostMapping("/post")
    void post(@RequestBody PostBody body) {
        System.out.println(body);
    }
}

@Value
@Builder
@JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
class PostBody {

    byte[] id;

    ClientData clientData;

    @JsonPOJOBuilder(withPrefix = "")
    public static class PostBodyBuilder {}
}

@Value
@Builder
@JsonDeserialize(builder = ClientData.ClientDataBuilder.class)
class ClientData {
    String something;
    Integer somethingElse;


    @JsonPOJOBuilder(withPrefix = "")
    public static class ClientDataBuilder {}
}

This works as fine, as you'd expect, with a normal JSON payload e.g:

{
  "id": "c29tZWlk",
  "clientData": {
    "something": "somethingValue",
    "somethingElse": 1
  }
}

However, we have a use-case where the clientData structure is known but, for reasons, is sent as a base64 encoded, stringified JSON blob e.g:

{
  "id": "c29tZWlk",
  "clientData": "eyJzb21ldGhpbmciOiJzb21ldGhpbmdWYWx1ZSIsInNvbWV0aGluZ0Vsc2UiOjF9"
}

It would be great if we could transparently decode and de-stringify this field as part of the deserialisation of PostBody before it calls runs the deserializer for ClientData.

One solution is create a custom deserialiser for PostBody, but in a real example there are a lot more fields that would then need to be handled manually.

I've tried creating a custom ClientData deserializer, but I'm struggling to understand the myriad of different types of desrializer interfaces available.

I've got something like this so far:

@Value
@Builder
@JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
class PostBody {

    byte[] id;

    @JsonDeserialize(using = ClientDataBase64Deserializer.class)
    ClientData clientData;

    @JsonPOJOBuilder(withPrefix = "")
    public static class PostBodyBuilder {}
}

// SNIP

class ClientDataBase64Deserializer extends StdScalarDeserializer<ClientData> {

    protected ClientDataBase64Deserializer() {
        super(ClientData.class);
    }

    @Override
    public ClientData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
        byte[] value = Base64.getDecoder().decode(jsonParser.getText());
        System.out.println(new String(value)); // prints stringified JSON
        jsonParser.setCurrentValue(/* somehow convert stringified JSON to a Tree Node? */ value);
        return deserializationContext.readValue(jsonParser, ClientData.class);
    }
}

I'd be grateful for any ideas on how to progress with this example, or some other mechanism that I may be missing to solve this problem?

Cheers

Upvotes: 1

Views: 1413

Answers (1)

djrollins
djrollins

Reputation: 300

In true SO fashion, I managed to solve my problem minutes after I asked the question.

This implementation of ClientDataBase64Deserializer and PostBody works as expected:

@Value
@Builder
@JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
public class PostBody {

    byte[] id;

    ClientData clientData;

    public interface IPostBodyBuilder {
        @JsonDeserialize(using = ClientDataBase64Deserializer.class)
        PostBody.PostBodyBuilder clientData(ClientData clientData);
    }

    @JsonPOJOBuilder(withPrefix = "")
    public static class PostBodyBuilder implements IPostBodyBuilder {}
}


class ClientDataBase64Deserializer extends StdScalarDeserializer<ClientData> {
    private final ObjectMapper objectMapper;

    protected ClientDataBase64Deserializer(ObjectMapper objectMapper) {
        super(ClientData.class);
        this.objectMapper = objectMapper;
    }

    @Override
    public ClientData deserialize(
            JsonParser jsonParser, DeserializationContext deserializationContext
    ) {
        byte[] value = jsonParser.readValueAs(byte[].class);
        return objectMapper.readValue(value, ClientData.class);
    }
}

Upvotes: 1

Related Questions