How to configure Gson to do additional processing on the value for toJson?
public class MyClass{
#SerializedName("qwerty")
#Mask(exposeFront=2, exposeRear=2, mask="*")
private String qwerty
}
Assuming MyClass#qwerty has a value of 1234567890, how to set Gson to output {"qwerty":"12******90"}?
Gson ReflectiveTypeAdapterFactory, that is responsible for "plain" objects serialization and deserialization, is not possible to enhance to support any other annotations like #Masked. It can only use annotations like #Expose (indirectly via an exclusion strategy), #SerializedName and a few others like #Since and #Until (exclusion strategy too). Note these annotations are documented and supported by default. In general, Gson suggests using a type adapter for the declaring class, MyClass, but this also means that you must manage all fields and make sure the corresponding type adapter is updated once your class is changed. Even worse, adding a custom type adapter makes these annotations support lost.
As an another way of working around it is injecting a special string type adapter factory that can do the trick, but due to the mechanics of how it is injected, this is both limited and requires duplicating the #Masked annotation values (if you're using the annotation elsewhere in your code) and the type adapter factory configuration in #JsonAdapter.
public abstract class MaskedTypeAdapterFactory
implements TypeAdapterFactory {
private final int exposeFront;
private final int exposeRear;
private final char mask;
private MaskedTypeAdapterFactory(final int exposeFront, final int exposeRear, final char mask) {
this.exposeFront = exposeFront;
this.exposeRear = exposeRear;
this.mask = mask;
}
// must be "baked" into the class (name only represents the configuration)
public static final class _2_2_asterisk
extends MaskedTypeAdapterFactory {
private _2_2_asterisk() {
super(2, 2, '*');
}
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( typeToken.getRawType() != String.class ) {
return null;
}
#SuppressWarnings("unchecked")
final TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getAdapter(typeToken);
final TypeAdapter<String> typeAdapter = new TypeAdapter<String>() {
#Override
public void write(final JsonWriter out, final String value)
throws IOException {
// mask the value
final int length = value.length();
final char[] buffer = value.toCharArray();
for ( int i = exposeFront; i < length - exposeRear; i++ ) {
buffer[i] = mask;
}
out.value(new String(buffer));
}
#Override
public String read(final JsonReader in)
throws IOException {
return delegate.read(in);
}
}
.nullSafe();
#SuppressWarnings("unchecked")
final TypeAdapter<T> adapter = (TypeAdapter<T>) typeAdapter;
return adapter;
}
}
#NoArgsConstructor
#AllArgsConstructor
final class MyClass {
#SerializedName("qwerty")
#Mask(exposeFront = 2, exposeRear = 2, mask = "*")
// unfortunately, this must duplicate the #Mask annotation values
// since type adapter (factories) do not accept supplemental information
// and Java annotations can only accept compile-time constants
#JsonAdapter(MaskedTypeAdapterFactory._2_2_asterisk.class)
#SuppressWarnings("unused")
private String qwerty;
}
Test:
public final class MaskedTypeAdapterFactoryTest {
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
final String actual = gson.toJson(new MyClass("1234567890"));
final String expected = "{\"qwerty\":\"12******90\"}";
Assertions.assertEquals(expected, actual);
}
}
This is probably the most robust way of doing that in Gson.
Related
I have a message in JSON format that I converted to a JSONObject, and I have around 30 mandatory fields that I have to check for whether they're null or not. If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message. Is there any efficient way I can do this without going through each and every field and using isNull() ?
Also, the JSON objects are nested, so a simple anyNull() function would not work since it would only return if the object itself is null and not if the variables themselves are null.
I tried using gson to convert the message to a POJO, and created classes for 10 objects
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
but since many classes are nested (and one of which is an array of objects) using simple null checkers don't work.
Actually speaking your question is not very clear because you're using a word of "message" that refers your particular class, but can also be more generic referring sent/received messages.
So something like for JSON elements in memory:
public static void failOnNullRecursively(final JsonElement jsonElement) {
if ( jsonElement.isJsonNull() ) {
throw new IllegalArgumentException("null!");
}
if ( jsonElement.isJsonPrimitive() ) {
return;
}
if ( jsonElement.isJsonArray() ) {
for ( final JsonElement element : jsonElement.getAsJsonArray() ) {
failOnNullRecursively(element);
}
return;
}
if ( jsonElement.isJsonObject() ) {
for ( final Map.Entry<String, JsonElement> e : jsonElement.getAsJsonObject().entrySet() ) {
failOnNullRecursively(e.getValue());
}
return;
}
throw new AssertionError(jsonElement);
}
or JSON documents in streams:
public final class FailOnNullJsonReader
extends JsonReader {
private FailOnNullJsonReader(final Reader reader) {
super(reader);
}
public static JsonReader create(final Reader reader) {
return new FailOnNullJsonReader(reader);
}
#Override
public void nextNull() {
throw new IllegalStateException(String.format("null at %#!", getPath()));
}
}
Both of them will throw on null. But it also seems that you want to validate Message instances:
If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message.
So this tells why the above null-checks won't fit your needs. What you're looking for is JSR-303. It won't be that efficient as you might want to want it to be (message instances are deserialized, validation takes time and resources too), but it might be efficient from the coding perspective:
final Set<ConstraintViolation<V>> violations = validator.validate(message);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
or even integrate it right into Gson so that it serves middleware:
public final class PostReadTypeAdapterFactory<V>
implements TypeAdapterFactory {
private final Predicate<? super TypeToken<?>> supports;
private final BiConsumer<? super TypeToken<V>, ? super V> onRead;
private PostReadTypeAdapterFactory(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
this.supports = supports;
this.onRead = onRead;
}
public static <V> TypeAdapterFactory create(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
return new PostReadTypeAdapterFactory<>(supports, onRead);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !supports.test(typeToken) ) {
return null;
}
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T readValue = delegate.read(in);
#SuppressWarnings("unchecked")
final V value = (V) readValue;
#SuppressWarnings("unchecked")
final TypeToken<V> valueTypeToken = (TypeToken<V>) typeToken;
onRead.accept(valueTypeToken, value);
return readValue;
}
};
}
}
public final class Jsr303Support {
private Jsr303Support() {
}
public static <V> TypeAdapterFactory createTypeAdapterFactory(final Validator validator) {
return PostReadTypeAdapterFactory.<V>create(
typeToken -> typeToken.getRawType().isAnnotationPresent(Validate.class),
(typeToken, value) -> {
final Set<ConstraintViolation<V>> violations = validator.validate(value);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
}
);
}
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface Validate {
}
And the test (using Lombok for brevity):
#Validate
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#NotNull
final String foo;
#NotNull
final String bar;
#NotNull
final String baz;
}
public final class Jsr303SupportTest {
private static final Validator validator;
static {
try ( final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory() ) {
validator = validatorFactory.getValidator();
}
}
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(Jsr303Support.createTypeAdapterFactory(validator))
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final ConstraintViolationException ex = Assertions.assertThrows(ConstraintViolationException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals(1, ex.getConstraintViolations().size());
}
}
And finally, probably the most efficient (in terms of reading JSON stream), but very limited whencompared to JSR-303 (and NOT working in Gson because Gson does not propagate null-checking to downstream (de)serializers), way that could replace #NotNull with a similar "functional" annotation:
public final class NotNullTypeAdapterFactory
implements TypeAdapterFactory {
// note no external access
private NotNullTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> delegate = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, #Nullable final T value)
throws IOException {
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null");
}
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
#Nullable
final T value = delegate.read(in);
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null at " + in.getPath());
}
return value;
}
};
}
}
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String foo;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String bar;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String baz;
}
public final class NotNullTypeAdapterFactoryTest {
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals("whatever here, the above does not work anyway", ex.getMessage());
}
}
The third, JSR-303, looks like the best for you.
Could I get #SerializedName value from config file etc.?
I mean like:
#SerializedName(value = configProfider.getJsonFieldName())
private String myField
Thanks.
I was particularly wrong in my comment: this is possible in Gson, but not with #SerializedName (due to how Gson works internally) and non-compile time expressions (due to Java restrictions). Sorry for the confusion. Gson supports a custom field naming strategy that you can supply while configuring your Gson instance.
Consider the following custom annotation:
#Retention(RUNTIME)
#Target(FIELD)
#interface DynamicSerializedName {
String value();
}
Now just implement your strategy:
final class DynamicSerializedNameStrategy
implements FieldNamingStrategy {
private final Function<? super String, String> translator;
private DynamicSerializedNameStrategy(final Function<? super String, String> translator) {
this.translator = translator;
}
static FieldNamingStrategy getDynamicSerializedNameStrategy(final Function<? super String, String> translator) {
return new DynamicSerializedNameStrategy(translator);
}
#Override
public String translateName(final Field field) {
final DynamicSerializedName annotation = field.getAnnotation(DynamicSerializedName.class);
if ( annotation == null ) {
// Taking the default naming strategy
// #SerializedName takes higher priority in ReflectiveTypeAdapterFactory.Adapter anyway
return FieldNamingPolicy.IDENTITY.translateName(field);
}
final String key = annotation.value();
final String resolvedName = translator.apply(key);
if ( resolvedName == null ) {
throw new IllegalArgumentException("Cannot resolve name by " + key + " for " + field);
}
return resolvedName;
}
}
Create an annotation:
final class Model {
#DynamicSerializedName("gson.model.field")
final String field = null;
}
How it can be used (system properties-based example):
private static final Gson gson = new GsonBuilder()
.setFieldNamingStrategy(getDynamicSerializedNameStrategy(System::getProperty))
.create();
public static void main(final String... args)
throws IOException {
try ( final Reader reader = getPackageResourceReader(Q43517297.class, "arbitrary.json") ) {
final Model model = gson.fromJson(reader, Model.class);
System.out.println(model.field);
}
}
This example will fail unless you do:
either define the gson.model.field system property programmatically like System.setProperty("gson.model.field", "dynamic");;
or define the system property using the -D parameter while starting the JVM: -Dgson.model.field=dynamic.
Consider the following JSON (referenced as arbitrary.json above):
{
"static": "BY-STATIC-NAME",
"dynamic": "BY-DYNAMIC-NAME"
}
Once the #DynamicSerializedName annotation is set and the corresponding property is properly configured the output will be:
BY-DYNAMIC-NAME
Once you remove the #DynamicSerializedName annotation from the DTO field, or annotate the field with #SerializedName (it has higher priority as of Gson 2.8.0), the output will be:
BY-STATIC-NAME
I have an Socket-Server in Java. This socket will receive json-strings with an specific structure.
{
"command": "test",
"name": "Hallo Welt"
}
I can not change this structure. The value of "command" will declare the type of content.
After I receive this from the socket, I would like to call different handlers, to handle these different commands:
command "test" > TestHandler implements CommandHandler
command "foo" > FooHandler implements CommandHandler
How can I convert the json into a object and bind the object to the specific handler?
This is my current approach:
I have an model class called BaseCommand which contains a enum command field.
class BaseCommand {
public CommandType command;
}
class TestCommand extends BaseCommand {
public String name;
}
With GSON I parse the JSON to BaseCommand class.
After that I can read the command type.
I declare a ENUM to map the command types to the Handler:
enum CommandType {
test(TestHandler.class),
foo(FooHandler.class);
public final Class<? extends CommandHandler> handlerClass;
public CommandTypes(Class<? extends CommandHandler> handlerClass) {
this.handlerClass = handlerClass;
}
}
My handler's are implementing this interface:
public interface CommandHandler<T extends BaseCommand> {
void handle(T command);
}
Now I have the command type enum and through Google Guices MapBinder I can get the Handler instance to handle request. This works
// in class ...
private final Map<CommandType, CommandHandler> handlers;
#Inject ClassName(Map<CommandType, CommandHandler> handlers) {
this.handlers = handlers;
}
// in converter method
private void convert(String json) {
BaseCommand baseCommand = GSONHelper().fromJson(json, BaseCommand.class);
// How can I get the CommandModel?
// If the commandType is "test" how can I parse TestCommand automatically?
??? commandModel = GSONHelper().fromJson(json, ???);
handlers.get(baseCommand.command).handle(commandModel);
}
Does anyone know a solution for my problem?
Or a complete different approach for this?
best regards, Michael
How can I get the CommandModel?
If the commandType is "test" how can I parse TestCommand automatically?
You can use a TypeAdapterFactory to get the most appropriate type adapter in the most accurate and flexible way. The example below slightly differs from your classes naming, but I think it's not a big issue to you. So, let's assume you have the following command arguments DTO declarations:
abstract class AbstractCommandDto {
final String command = null;
}
final class HelloCommandDto
extends AbstractCommandDto {
final String name = null;
}
Now you can make a special TypeAdapterFactory to make a sort of looking-ahead to determine the incoming command by command arguments name. It may look complicated, but in fact TypeAdapterFactoryies are not that hard to implement. Note that JsonDeserializer might be another option for you, but then you lose automatic deserializing unless you delegate its deserialize() method to another backing Gson instance.
final class AbstractCommandDtoTypeAdapterFactory
implements TypeAdapterFactory {
// The factory handles no state and can be instantiated once
private static final TypeAdapterFactory abstractCommandDtoTypeAdapterFactory = new AbstractCommandDtoTypeAdapterFactory();
// Type tokens are used to define type information and are perfect value types so they can be instantiated once as well
private static final TypeToken<CommandProbingDto> abstractCommandProbingDtoTypeToken = new TypeToken<CommandProbingDto>() {
};
private static final TypeToken<HelloCommandDto> helloCommandDtoTypeToken = new TypeToken<HelloCommandDto>() {
};
private AbstractCommandDtoTypeAdapterFactory() {
}
static TypeAdapterFactory getAbstractCommandDtoTypeAdapterFactory() {
return abstractCommandDtoTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// First, check if the incoming type is AbstractCommandDto
if ( AbstractCommandDto.class.isAssignableFrom(typeToken.getRawType()) ) {
// If yes, then build a special type adapter for the concrete type
final TypeAdapter<AbstractCommandDto> abstractCommandDtoTypeAdapter = new AbstractCommandDtoTypeAdapter(
gson,
gson.getDelegateAdapter(this, abstractCommandProbingDtoTypeToken),
(commandName, jsonObject) -> deserialize(gson, commandName, jsonObject),
dto -> getTypeAdapter(gson, dto)
);
// Some cheating for javac...
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) abstractCommandDtoTypeAdapter;
return typeAdapter;
}
// If it's something else, just let Gson pick up the next type adapter
return null;
}
// Create an AbstractCommandDto instance out of a ready to use JsonObject (see the disadvantages about JSON trees below)
private AbstractCommandDto deserialize(final Gson gson, final String commandName, final JsonObject jsonObject) {
#SuppressWarnings("unchecked")
final TypeToken<AbstractCommandDto> typeToken = (TypeToken<AbstractCommandDto>) resolve(commandName);
final TypeAdapter<AbstractCommandDto> typeAdapter = gson.getDelegateAdapter(this, typeToken);
return typeAdapter.fromJsonTree(jsonObject);
}
private TypeAdapter<AbstractCommandDto> getTypeAdapter(final Gson gson, final AbstractCommandDto dto) {
#SuppressWarnings("unchecked")
final Class<AbstractCommandDto> clazz = (Class<AbstractCommandDto>) dto.getClass();
return gson.getDelegateAdapter(this, TypeToken.get(clazz));
}
// Or any other way to resolve the class. This is just for simplicity and can be even extract elsewhere from the type adapter factory class
private static TypeToken<? extends AbstractCommandDto> resolve(final String commandName)
throws IllegalArgumentException {
switch ( commandName ) {
case "hello":
return helloCommandDtoTypeToken;
default:
throw new IllegalArgumentException("Cannot handle " + commandName);
}
}
private static final class AbstractCommandDtoTypeAdapter
extends TypeAdapter<AbstractCommandDto> {
private final Gson gson;
private final TypeAdapter<CommandProbingDto> probingTypeAdapter;
private final BiFunction<? super String, ? super JsonObject, ? extends AbstractCommandDto> commandNameToCommand;
private final Function<? super AbstractCommandDto, ? extends TypeAdapter<AbstractCommandDto>> commandToTypeAdapter;
private AbstractCommandDtoTypeAdapter(
final Gson gson,
final TypeAdapter<CommandProbingDto> probingTypeAdapter,
final BiFunction<? super String, ? super JsonObject, ? extends AbstractCommandDto> commandNameToCommand,
final Function<? super AbstractCommandDto, ? extends TypeAdapter<AbstractCommandDto>> commandToTypeAdapter
) {
this.gson = gson;
this.probingTypeAdapter = probingTypeAdapter;
this.commandNameToCommand = commandNameToCommand;
this.commandToTypeAdapter = commandToTypeAdapter;
}
#Override
public void write(final JsonWriter out, final AbstractCommandDto dto)
throws IOException {
// Just pick up a delegated type adapter factory and use it
// Or just throw an UnsupportedOperationException if you're not going to serialize command arguments
final TypeAdapter<AbstractCommandDto> typeAdapter = commandToTypeAdapter.apply(dto);
typeAdapter.write(out, dto);
}
#Override
public AbstractCommandDto read(final JsonReader in) {
// Here you can two ways:
// * Either "cache" the whole JSON tree into memory (JsonElement, etc,) and simplify the command peeking
// * Or analyze the JSON token stream in a more efficient and sophisticated way
final JsonObject jsonObject = gson.fromJson(in, JsonObject.class);
final CommandProbingDto commandProbingDto = probingTypeAdapter.fromJsonTree(jsonObject);
// Or just jsonObject.get("command") and even throw abstractCommandDto, AbstractCommandProbingDto and all of it gets away
final String commandName = commandProbingDto.command;
return commandNameToCommand.apply(commandName, jsonObject);
}
}
// A synthetic class just to obtain the command field
// Gson cannot instantiate abstract classes like what AbstractCommandDto is
private static final class CommandProbingDto
extends AbstractCommandDto {
}
}
And how it's used:
public static void main(final String... args) {
// Build a command DTO-aware Gson instance
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getAbstractCommandDtoTypeAdapterFactory())
.create();
// Build command registry
final Map<Class<?>, Consumer<?>> commandRegistry = new LinkedHashMap<>();
commandRegistry.put(HelloCommandDto.class, new HelloCommand());
// Simulate and accept a request
final AbstractCommandDto abstractCommandDto = gson.fromJson("{\"command\":\"hello\",\"name\":\"Welt\"}", AbstractCommandDto.class);
// Resolve a command
final Consumer<?> command = commandRegistry.get(abstractCommandDto.getClass());
if ( command == null ) {
throw new IllegalArgumentException("Cannot handle " + abstractCommandDto.command);
}
// Dispatch
#SuppressWarnings("unchecked")
final Consumer<AbstractCommandDto> castCommand = (Consumer<AbstractCommandDto>) command;
castCommand.accept(abstractCommandDto);
// Simulate a response
System.out.println(gson.toJson(abstractCommandDto));
}
private static final class HelloCommand
implements Consumer<HelloCommandDto> {
#Override
public void accept(final HelloCommandDto helloCommandDto) {
System.out.println("Hallo " + helloCommandDto.name);
}
}
The output:
Hallo Welt
I am writing a Java library for interacting with metrics from Graphite.
A typical JSON response looks like this (taken from the official docs):
[{
"target": "entries",
"datapoints": [
[1.0, 1311836008],
[2.0, 1311836009],
[3.0, 1311836010],
[5.0, 1311836011],
[6.0, 1311836012]
]
}]
where the first element of the "datapoints" array is the value and the second one the timestamp. I have modelled a GraphiteDataset class as follows
class GraphiteDataset {
private String target;
private List<GraphiteDatapoint> datapoints;
....
}
and the GraphiteDatapoint class
class GraphiteDatapoint {
private Long timestamp;
private Double value;
...
}
Now I need to parse the response (see above) into the GraphiteDataset
class using Gson. Unfortunately, the elements of "datapoints" are not named objects (e.g. {timestamp: 1234, value: 1.0} but a 2 dimensional array so I cannot directly deserialize it into some class. Currently my solution is to have an intermediate class
class GraphiteIntermediateDataset {
private String target;
private List<String> datapoints;
...
}
which has the datapoints as Strings and then I parse them into the appropriate GraphiteDatapoint instance. I think that I cannot work around a custom deserializer. Do you have any suggestions or tricks how to make this a little more convenient?
The JSON [1.2, 123456] is a array of a Double and a Long, but they are both Number, so try this:
class GraphiteDataset {
private String target;
private List<List<Number>> datapoints;
....
}
Then convert datapoints into your type after parsing, with something like:
List<GraphiteDatapoint> points = datapoints.stream().
.map(nums -> new GraphiteDatapoint(nums.get(0).doubleValue(), nums.get(1).intValue()))
.collect(Collectors.toList());
assuming a constructor like:
class GraphiteDatapoint {
private Long timestamp;
private Double value;
public GraphiteDatapoint(Double value, Long timestamp) {
this.value = value;
this.timestamp = timestamp;
}
...
}
The final solution is to introduce an intermediate class GraphiteIntermediateDataset which looks as follows:
class GraphiteIntermediateDataset {
private String target;
private List<List<Number>> datapoints;
}
and the deserializer code looks like this
List<GraphiteIntermediateDataset> intermediateDatasetList = GSON.fromJson(raw, new TypeToken<List<GraphiteIntermediateDataset>>(){}.getType());
GraphiteIntermediateDataset intermediateDataset = intermediateDatasetList.get(0);
... check if empty (which can happen), when true return an empty GraphiteDataset
List<GraphiteDatapoint> gDatapoints = intermediateDataset
.stream()
.map(ds -> {
return new GraphiteDatapoint(ds.get(0).longValue(),
ds.get(1).doubleValue())
}
.collect(Collectors.toList());
return new GraphiteDataset()
.setDatapoints(gDatapoints);
Type safety and proper data binding are your friends. Gson has several methods to accomplish what you need. For example, declare data transfer objects:
final class GraphiteDataset {
final String target;
// The incoming DTO has property `datapoints`, however Java conventions suggest dataPoints (as far as I understand English).
#SerializedName("datapoints")
final List<GraphiteDataPoint> dataPoints;
// Actually, Gson does not need this constructor, and the DTO can even have a single private default one.
// But in order to make it consistent with the next class just making it programmatically instantiable...
// Also, but may be opinion-based, hiding constructors is really a good idea since one can hide the instantiation strategy whilst constructors cannot.
private GraphiteDataset(final String target, final List<GraphiteDataPoint> dataPoints) {
this.target = target;
this.dataPoints = dataPoints;
}
}
final class GraphiteDataPoint {
final double value;
final long timestamp;
private GraphiteDataPoint(final double value, final long timestamp) {
this.value = value;
this.timestamp = timestamp;
}
// Instantiation must be accessible programmatically somehow
static GraphiteDataPoint graphiteDataPoint(final double value, final long timestamp) {
return new GraphiteDataPoint(value, timestamp);
}
}
And then implement either a GraphiteDataPoint JSON deserializer:
// In Gson serializers and deserializers can only deal with intermediate Gson JSON tree representation of objects (JsonElement-s).
// For some cases it's quite simple, if the given data to serialize/deserialize does not consume much memory
final class GraphiteDataPointJsonDeserializer
implements JsonDeserializer<GraphiteDataPoint> {
private static final JsonDeserializer<GraphiteDataPoint> graphiteDataPointJsonDeserializer = new GraphiteDataPointJsonDeserializer();
private GraphiteDataPointJsonDeserializer() {
}
// Not letting to instantiate a stateless (so it's thread-safe) deserializer twice or more
static JsonDeserializer<GraphiteDataPoint> getGraphiteDataPointJsonDeserializer() {
return graphiteDataPointJsonDeserializer;
}
#Override
public GraphiteDataPoint deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
final JsonArray asJsonArray = jsonElement.getAsJsonArray();
final double value = asJsonArray.get(0).getAsJsonPrimitive().getAsDouble();
final long timestamp = asJsonArray.get(1).getAsJsonPrimitive().getAsLong();
return graphiteDataPoint(value, timestamp);
}
}
Or a type adapter:
// Type adapters, unlike serializers and deserializers, are designed to work with streams.
// They may look too low-level and tedious/hard to implement, but for some cases they can be useful in both serialization and deserialization.
// For the case #1: no need to serialize nested objects recursively to transform them to JSON trees that can be important for large objects.
// For the case #2: intermediate JSON trees are not necessary (but internal buffers are).
final class GraphiteDataPointTypeAdapter
extends TypeAdapter<GraphiteDataPoint> {
private static final TypeAdapter<GraphiteDataPoint> graphiteDataPointTypeAdapter = new GraphiteDataPointTypeAdapter();
private GraphiteDataPointTypeAdapter() {
}
static TypeAdapter<GraphiteDataPoint> getGraphiteDataPointTypeAdapter() {
return graphiteDataPointTypeAdapter;
}
#Override
public void write(final JsonWriter out, final GraphiteDataPoint value) {
throw new UnsupportedOperationException("not implemented");
}
#Override
public GraphiteDataPoint read(final JsonReader in)
throws IOException {
in.beginArray();
final double value = in.nextDouble();
final long timestamp = in.nextLong();
in.endArray();
return graphiteDataPoint(value, timestamp);
}
}
Both implementations are essentially the same, but may be crucial for you dependening on data (de)serialization strategies and costs. Example use:
private static final String JSON = "[{\"target\":\"entries\",\"datapoints\":[[1.0,1311836008],[2.0,1311836009],[3.0,1311836010],[5.0,1311836011],[6.0,1311836012]]}]";
// Gson is thread-safe and can be shared between threads, so no need to instantiate it every time it's needed
private static final Gson gsonWithDeserializers = new GsonBuilder()
.registerTypeAdapter(GraphiteDataPoint.class, getGraphiteDataPointJsonDeserializer())
.create();
private static final Gson gsonWithTypeAdapters = new GsonBuilder()
.registerTypeAdapter(GraphiteDataPoint.class, getGraphiteDataPointTypeAdapter())
.create();
private static final TypeToken<List<GraphiteDataset>> graphiteDatasetsTypeToken = new TypeToken<List<GraphiteDataset>>() {
};
public static void main(final String... args) {
dumpGraphiteDatasets(gsonWithDeserializers.fromJson(JSON, graphiteDatasetsTypeToken.getType()));
dumpGraphiteDatasets(gsonWithTypeAdapters.fromJson(JSON, graphiteDatasetsTypeToken.getType()));
}
private static void dumpGraphiteDatasets(final Iterable<GraphiteDataset> graphiteDatasets) {
graphiteDatasets.forEach(graphiteDataset -> {
out.println(graphiteDataset.target);
graphiteDataset.dataPoints.forEach(graphiteDataPoint -> {
out.print(" ");
out.print(graphiteDataPoint.value);
out.print(" ");
out.println(graphiteDataPoint.timestamp);
});
});
}
The output:
entries
1.0 1311836008
2.0 1311836009
3.0 1311836010
5.0 1311836011
6.0 1311836012
entries
1.0 1311836008
2.0 1311836009
3.0 1311836010
5.0 1311836011
6.0 1311836012
Is it possible to using retrofit in a manner that uses generic type?
for example something like this:
public interface RetroInterface<T> {
#GET("content/{id}")
T getById(#Path("id") int id);
}
I've read that Retrofit uses the method signature to determine the return Type at runtime, in that case is it even possible to have generic interface such as above?
Yes I think it's possible but be carefull retrofit return some Call So you can create an interface with Call<T> like method except
But Have you really need to create a template for a service ? Because in you get annotations you ask to server one specific ressource so you known the type of response
The only way to pass that info I can think of is introducing a wrapper to hold both value and its type (or type token to simplify Gson).
final class GenericBody<T> {
final T body;
final TypeToken<T> typeToken;
GenericBody(final T body, final TypeToken<T> typeToken) {
this.body = body;
this.typeToken = typeToken;
}
}
Then an example service might be declared as follows:
interface IGenericService {
#POST("/")
Call<Void> post(#Body #SuppressWarnings("rawtypes") GenericBody genericBody);
}
Here, the Call is declared to return nothing, and genericBody is intentionally made raw-typed to let it pass Retrofit validation.
Next, the Gson part.
final class GenericBodyTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory genericBodyTypeAdapterFactory = new GenericBodyTypeAdapterFactory();
private GenericBodyTypeAdapterFactory() {
}
static TypeAdapterFactory getGenericBodyTypeAdapterFactory() {
return genericBodyTypeAdapterFactory;
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !GenericBody.class.isAssignableFrom(typeToken.getRawType()) ) {
return null;
}
final TypeAdapter<GenericBody<T>> genericBodyTypeAdapter = new TypeAdapter<GenericBody<T>>() {
#Override
public void write(final JsonWriter out, final GenericBody<T> value)
throws IOException {
final T body = value.body;
final TypeAdapter<T> typeAdapter = gson.getDelegateAdapter(GenericBodyTypeAdapterFactory.this, value.typeToken);
typeAdapter.write(out, body);
}
#Override
public GenericBody<T> read(final JsonReader in) {
throw new UnsupportedOperationException();
}
};
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) genericBodyTypeAdapter;
return typeAdapter;
}
}
What it does it is:
checks if it can handle GenericBody instances;
resolves appropriate type adapters for the by the bound type token;
writes the generic body value to the output.
No read is implemented.
Example of use (full of mocks (staticResponse(applicationJsonMediaType, "OK")) that can be easily translated to your code):
private static final TypeToken<List<String>> stringListTypeToken = new
TypeToken<List<String>>() {
};
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(getGenericBodyTypeAdapterFactory())
.create();
private static final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(staticResponse(applicationJsonMediaType, "OK"))
.build();
private static final Retrofit retrofit = new Retrofit.Builder()
.baseUrl("http://whatever")
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
private static final IGenericService genericService =
retrofit.create(IGenericService.class);
public static void main(final String... args)
throws IOException {
final GenericBody<List<String>> body = new GenericBody<>(asList("foo", "bar", "baz"),
stringListTypeToken);
genericService.post(body).execute();
}
This would write ["foo","bar","baz"] to the output stream respecting properly configured Gson (de)serialization strategies.