1

I am implementing a custom JsonDeserializer because of a business logic that is neccessary for processing. But some parts can be parsed standard way. Is this possible - process some elements myself and let some nested elements processed automatically?

This is JSON:

{
  "id": "10",
  "games": ["PZ"],
  "definition":
  {
    "count": 10,
    "operatorDefinitions": [
      {
        "operators": "+",
        "first": "1-5",
        "second": "1-5",
        "result": "2-5"
      }
    ]
  }

This is custome deserializer for definition item:

public class FormulaDefinitionGsonAdapter implements JsonDeserializer<FormulaDefinition> {
public FormulaDefinition deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
    FormulaDefinition definition = new FormulaDefinition();
    JsonObject jsonObject = json.getAsJsonObject();
    JsonPrimitive p = jsonObject.getAsJsonPrimitive("count");
    definition.setCount(p.getAsInt());

    JsonArray array = jsonObject.getAsJsonArray("operatorDefinitions");
    if (array == null || array.size() == 0) {
        throw new JsonParseException("Element 'operatorDefinitions' is missing!");
    }

    for (JsonElement jsonElement : array) {
        jsonObject = (JsonObject) jsonElement;
        p = jsonObject.getAsJsonPrimitive("first");
        String sValues = p.getAsString();
        Values firstArgValues = Values.parse(sValues);

And now I would like to let GSON parse operators enum. I can do it myself, it is just few lines of code, but I would prefer library do as much as it can.

Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Leos Literak
  • 8,805
  • 19
  • 81
  • 156

1 Answers1

4

... but I would prefer library do as much as it can.

Well, just use Gson.

There is the Data Transfer Object pattern, and Gson mapping classes in particular, that deal with your problem perfectly. By default, if Gson is capable to satisfy mappings by the built-in facilities, and you don't have to do its job yourself except of special cases. Such mapping classes are only meant to exist between JSON content and your business object class in order to (de)serialize the data (simply speaking, DTOs only exist for this purpose, and Gson-related annotations must not spread onto your business classes -- just convert DTOs to business objects).

Mappings

final class Wrapper {

    @SerializedName("id")
    @Expose
    private final String id = null;

    @SerializedName("games")
    @Expose
    private final List<String> games = null;

    @SerializedName("definition")
    @Expose
    private final FormulaDefinition formulaDefinition = null;

    private Wrapper() {
    }

    @Override
    public String toString() {
        return new StringBuilder("Wrapper{")
                .append("id='").append(id)
                .append("', games=").append(games)
                .append(", formulaDefinition=").append(formulaDefinition)
                .append('}')
                .toString();
    }

}
package q41323887;

import java.util.List;

import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;

final class FormulaDefinition {

    @SerializedName("count")
    @Expose
    private /*final*/ int count /*= 0*/; // Gson works with final primitives like `int` strangely

    @SerializedName("operatorDefinitions")
    @Expose
    private final List<OperatorDefinition> operatorDefinitions = null;

    private FormulaDefinition() {
    }

    @Override
    public String toString() {
        return new StringBuilder("FormulaDefinition{")
                .append("count=").append(count)
                .append(", operatorDefinitions=").append(operatorDefinitions)
                .append('}')
                .toString();
    }

}
final class OperatorDefinition {

    @SerializedName("operators")
    @Expose
    private final Operator operators = null;

    @SerializedName("first")
    @Expose
    private final String first = null;

    @SerializedName("second")
    @Expose
    private final String second = null;

    @SerializedName("result")
    @Expose
    private final String result = null;

    private OperatorDefinition() {
    }

    @Override
    public String toString() {
        return new StringBuilder("OperatorDefinition{")
                .append("operators=").append(operators)
                .append(", first='").append(first)
                .append("', second='").append(second)
                .append("', result='").append(result)
                .append("'}")
                .toString();
    }

}
enum Operator {

    PLUS("+"),
    MINUS("-"),
    ASTERISK("*"),
    SLASH("/");

    private static final Map<String, Operator> tokenToOperatorIndex = createTokenToOperatorIndexInJava8();

    private final String token;

    Operator(final String token) {
        this.token = token;
    }

    static Operator resolveOperator(final String token)
            throws NoSuchElementException {
        final Operator operator = tokenToOperatorIndex.get(token);
        if ( operator == null ) {
            throw new NoSuchElementException("Cannot resolve operator by " + token);
        }
        return operator;
    }

    private static Map<String, Operator> createTokenToOperatorIndex() {
        final Map<String, Operator> index = new HashMap<>();
        for ( final Operator operator : values() ) {
            index.put(operator.token, operator);
        }
        return unmodifiableMap(index);
    }

    private static Map<String, Operator> createTokenToOperatorIndexInJava8() {
        final Map<String, Operator> index = Stream.of(values())
                .collect(toMap(operator -> operator.token, identity()));
        return unmodifiableMap(index);
    }

}

Deserialization

Then, since your operators are meant to be effective enums, this is the only place where you really need a custom JSON deserializer just because Gson default rules are not aware of these rules.

final class OperatorJsonDeserializer
        implements JsonDeserializer<Operator> {

    private static final JsonDeserializer<Operator> operatorJsonDeserializer = new OperatorJsonDeserializer();

    private OperatorJsonDeserializer() {
    }

    static JsonDeserializer<Operator> getOperatorJsonDeserializer() {
        return operatorJsonDeserializer;
    }

    @Override
    public Operator deserialize(final JsonElement json, final Type type, final JsonDeserializationContext context)
            throws JsonParseException {
        try {
            final String token = json.getAsJsonPrimitive().getAsString();
            return resolveOperator(token);
        } catch ( final NoSuchElementException ex ) {
            throw new JsonParseException(ex);
        }
    }

}

Demo

Now you can just use the Wrapper class to deserialize your JSON:

// Gson instances are thread-safe and can be easily instantiated once
private static final Gson gson = new GsonBuilder()
        .registerTypeAdapter(Operator.class, getOperatorJsonDeserializer())
        .create();

public static void main(final String... args)
        throws IOException {
    try ( final Reader reader = new InputStreamReader(EntryPoint.class.getResourceAsStream("/test.json")) ) {
        final Wrapper wrapper = gson.fromJson(reader, Wrapper.class);
        out.println(wrapper);
        // ... convert the wrapper DTO above to your target business object
    }
}

Output:

Wrapper{id='10', games=[PZ], formulaDefinition=FormulaDefinition{count=10, operatorDefinitions=[OperatorDefinition{operators=PLUS, first='1-5', second='1-5', result='2-5'}]}}


Edit

I was wrong about Gson in the following code snippet:

    @SerializedName("count")
    @Expose
    private /*final*/ int count /*= 0*/; // Gson works with final primitives like `int` strangely

Actually Gson does work fine. I forgot Java constants inlining. Getting the count via reflection using Field works perfect. However, the contant values are returned because of inlining. A similar plain object with javap -p -c:

final class ext.Test$Immutable {
  private final int foo;

  private ext.Test$Immutable();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field foo:I
       9: return

  private int getFoo();
    Code:
       0: iconst_0
       1: ireturn

  public java.lang.String toString();
    Code:
       0: ldc           #4                  // String (IMMUTABLE:0)
       2: areturn
}

In this case even toString() returns a constant. Yes, this is how Java and javac work. In order to disable such inlining and add the final modifier to the field like similarly to all fields around, a non-compile-time value should be added:

    @SerializedName("count")
    @Expose
    private final int count = constOf(0);

where constOf(int) is merely:

private static int constOf(final int value) {
    return value;
}

Now all incoming DTO fields can be easily declared final.

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105