... 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
.