0

I want to use Jackson to implement toString() to return the JSON representation of an object, but I do not want to use any Jackson annotation in my code.

I tried an implementation along the lines of:

public String toString()
{
    Map<String,Object> ordered = ImmutableMap.<String, Object>builder().
        put("createdAt", createdAt.toString()).
        put("address", address.toString()).
        build();

    ObjectMapper om = new ObjectMapper();
    om.enable(SerializationFeature.INDENT_OUTPUT);
    try
    {
        return om.writeValueAsString(object);
    }
    catch (JsonProcessingException e)
    {
        // Unexpected
        throw new AssertionError(e);
    }
}

This works well for simple fields but if "address" has its own fields then instead of getting this:

{
  "address" : {
    "value" : "AZ4RPBb1kSkH4RNewi4NXNkBu7BX9DmecJ",
    "tag" : null
}

I get this output instead:

{
  "address" : "{\n\"value\" : \"AZ4RPBb1kSkH4RNewi4NXNkBu7BX9DmecJ\",\n        \"tag\" : null"
}

In other words, the address value is being treated like a String as opposed to a JsonNode.

To clarify:

  • On the one hand, I want to control how simple class fields are converted to String. I don't want to use Jackson's built-in converter.
  • On the other hand, for complex fields, returning a String value to Jackson leads to the wrong behavior.

I believe that I could solve this problem by adding a public toJson() method to all my classes. That method would return a Map<String, JsonNode>, where the value is a string node for simple fields and the output of toJson() for complex fields. Unfortunately, this would pollute my public API with implementation details.

How can I achieve the desired behavior without polluting the class's public API?

UPDATE: I just saw an interesting answer at https://stackoverflow.com/a/9599585/14731 ... Perhaps I could convert the String value of complex fields back to JsonNode before passing them on to Jackson.

Gili
  • 86,244
  • 97
  • 390
  • 689
  • The problem is that your map stores a String, not an Object. The String stored there is `{\n\"value\" : \"AZ4RPBb1kSkH4RNewi4NXNkBu7BX9DmecJ\",\n \"tag\" : null`... – Luiggi Mendoza Jun 21 '18 at 22:10
  • @LuiggiMendoza I understand, but I don't want to leave it up to Jackson to convert objects to strings. I've updated the question with this clarification. – Gili Jun 21 '18 at 22:18
  • I still don't understand your purposes. Based on the description, you don't want to use `put("address", address)` but continue with `put("address", address.toString())` somehow. Maybe you can add a map instead of your object, like `put("address", ImmutableMap.builder().put("value", address.getValue()). put("tag", address.getTag()).build())`. – Luiggi Mendoza Jun 21 '18 at 22:21
  • @LuiggiMendoza For simple fields, I want to control the conversion directly. For some fields, I invoke `String.value()`. For others like `BigDecimal` I invoke `BigDecimal.toPlainString()`. For complex fields, I expect them to control their own conversion so I need them to return some value that I can pass to Jackson unmodified. – Gili Jun 21 '18 at 22:27

1 Answers1

1

I think you should implement two methods in each class - one to dump data, second to build JSON out of raw data structure. You need to separate this, otherwise you will nest it deeper and deeper every time you encapsulate nested toString() calls.

An example:

class Address {
    private BigDecimal yourField;

    /* …cut… */

    public Map<String, Object> toMap() {
        Map<String, Object> raw = new HashMap<>();
        raw.put("yourField", this.yourField.toPlainString());
        /* more fields */
        return raw;
    }

    @Override
    public String toString() {
        // add JSON processing exception handling, dropped for readability
        return new ObjectMapper().writeValueAsString(this.toMap());
    }
}

class Employee {
    private Address address;

    /* …cut… */

    public Map<String, Object> toMap() {
        Map<String, Object> raw = new HashMap<>();
        raw.put("address", this.address.toMap());
        /* more fields */
        return raw;
    }

    @Override
    public String toString() {
        // add JSON processing exception handling, dropped for readability
        return new ObjectMapper().writeValueAsString(this.toMap());
    }
}
Rafał Wrzeszcz
  • 1,996
  • 4
  • 23
  • 45
  • You are missing one requirement: I want to control the String representation of simple fields instead of letting Jackson handle the conversion. For example, I want to output `BigDecimal.toPlainString()` for `BigDecimal` and `Optional.orElse(null)` for `Optional` instead of Jackson's default representation. I assume that instead of `toMap()` returning the raw values I should return whatever String representation I want (for simple types only) and this will solve my problem. Do you agree? – Gili Jun 21 '18 at 23:10
  • That's the point of this separation - in `toMap()` you format field representation; in `toString()` you output JSON out of it. You should return whatever String representation you want from `toMap()`. `toMap()` can return in your case: `raw.put("decimalField", BigDecimal.toPlainString());` and in `toString()` this will be just formatted as JSON. You can think of it as a separation of concerns approach. I updated the answer to present this example. – Rafał Wrzeszcz Jun 22 '18 at 14:46