I have a json response that looks like this:
{
"id":"001",
"name":"Name",
"param_distance":"10",
"param_sampling":"2"
}
And I have two classes: Teste and Parameters
public class Test {
private int id;
private String name;
private Parameters params;
}
public class Parameters {
private double distance;
private int sampling;
}
My question is: is there a way to make Gson understand that some of the json attributes should go to the Parameters class, or the only way is to "manually" parse this ?
EDIT
Well, just to make my comment in #MikO's answer more readable:
I'll add a list of an object to the json output, so json response should look like this:
{
"id":"001",
"name":"Name",
"param_distance":"10",
"param_sampling":"2",
"events":[
{
"id":"01",
"value":"22.5"
},
{
"id":"02",
"value":"31.0"
}
]
}
And the Deserializer class would look like this:
public class TestDeserializer implements JsonDeserializer<Test> {
#Override
public Test deserialize(JsonElement json, Type type,
JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
Test test = new Test();
test.setId(obj.get("id").getAsInt());
test.setName(obj.get("name").getAsString());
Parameters params = new Parameters();
params.setDistance(obj.get("param_distance").getAsDouble());
params.setSampling(obj.get("param_sampling").getAsInt());
test.setParameters(params);
Gson eventGson = new Gson();
Type eventsType = new TypeToken<List<Event>>(){}.getType();
List<Event> eventList = eventGson.fromJson(obj.get("events"), eventsType);
test.setEvents(eventList);
return test;
}
}
And doing:
GsonBuilder gBuilder = new GsonBuilder();
gBuilder.registerTypeAdapter(Test.class, new TestDeserializer());
Gson gson = gBuilder.create();
Test test = gson.fromJson(reader, Test.class);
Gives me the test object the way I wanted.
The way to make Gson understand it is to write a custom deserializer by creating a TypeAdapter for your Test class. You can find information in Gson's User Guide. It is not exactly a manual parsing, but it is not that different, since you have to tell Gson how to deal with each JSON value...
It should be something like this:
private class TestDeserializer implements JsonDeserializer<Test> {
public Test deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
int id = obj.get("id").getAsInt();
String name = obj.get("name").getAsString();
double distance = obj.get("param_distance").getAsDouble();
int sampling = obj.get("param_sampling").getAsInt();
//assuming you have suitable constructors...
Test test = new Test(id, name, new Parameters(distance, sampling));
return test;
}
}
Then you have to register the TypeAdapter with:
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(Test.class, new TestDeserializer());
And finally you just have to parse your JSON as usual, with:
gson.fromJson(yourJsonString, Test.class);
Gson will automatically use your custom deserializer to parse your JSON into your Test class.
Related
How can I use Retrofit2 to parse these two kinds of API responses?
Ok response (HTTP 200):
{
"data": {
"foo": "bar"
}
}
Error response (HTTP 200):
{
"error": {
"foo": "bar"
}
}
I've read tons of SO questions and tutorials, but I don't know how to do that, I've tried:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapterFactory(new ItemTypeAdapterFactory());
Gson gson = gsonBuilder.create();
final Retrofit retrofit = new Retrofit.Builder()
.client(getOkHttpClient())
.baseUrl(Constants.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
And this is my ItemTypeAdapterFactory:
class ItemTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}
public T read(JsonReader in) throws IOException {
JsonElement jsonElement = elementAdapter.read(in);
if (jsonElement.isJsonObject()) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
// Data key
if (jsonObject.has(Constants.JSON_KEY_DATA)) {
JsonElement jsonData = jsonObject.get(Constants.JSON_KEY_DATA);
// Primitive
if (jsonData.isJsonPrimitive()) {
jsonElement = jsonData.getAsJsonPrimitive();
}
// JSON object
else if (jsonData.isJsonObject()) {
jsonElement = jsonData;
}
// JSON object array
else if (jsonData.isJsonArray()) {
jsonElement = jsonData.getAsJsonArray();
}
}
}
return delegate.fromJsonTree(jsonElement);
}
}.nullSafe();
}
}
But now I don't know the type to be declared on retrofit2 interface, inside Call:
#GET("login")
Call<?> login(#Query(Constants.API_PARAM_TOKEN) String token);
Could you please point me in the right direction?
In a similar case, I once used JsonObject as type, so your function will look like this:
#GET("login")
Call<?> login(#Query(Constants.API_PARAM_TOKEN) String token);
Next, when you make a retrofit call, you keep the response as a string. So, in your java code, do something like this:
Call<JsonObject> call = RetrofitClient.getAPIService().login('YOUR_INPUT');
Data data = null;
Error error = null;
call.enqueue(new Callback<JsonObject>() {
#Override
public void onResponse(Call<JsonObject> call, Response<JsonObject> response) {
if(response.isSuccessfull()){
String jsonString = response.body().toString();
if(jsonString.contains("data:")){
data = new Gson().fromJson(jsonString,Data.class);
}else{
error = new Gson().fromJson(jsonString,Error.class);
}
}
}
Here, I have used Data and Error these 2 classes. They are the POJOs. So Data can look something like this:
Data.java:
public class Data implements Serializable{
#SerializedName("foo")
#Expose
private Foo foo; // Foo is your desired data type
}
Same goes for Error. So depending on your rest of the code, make necessary changes. Good luck.
I used to do something like this
BaseResponse
public class BaseResponse<D,E>{
E error;
D data;
public boolean isSuccess(){
return error==null;
}
}
Retrofit interface
#GET("login")
Call<BaseResponse<LoginData,ErrorData>> login(#Query(Constants.API_PARAM_TOKEN) String token);
this approach will work OK when you have control over the REST API structure.
the only problem is that you need to check for success using isSuccess method for every request before using the data object.
I've been working with Retrofit on a couple of my projects before but now I want to do something slightly different. I'm calling an api that wraps my response in a structure similar to this:
{ // only for demo purposes. Probably errors and data will never be populated together
"body": {
"errors": {
"username": [
"Username is too short",
"Username already exists"
]
},
"data": {
"message": "User created."
}
}
}
I'm trying to convert all that to a generic class which will wrap that response for me. What I have in mind is something like
public class ApiResponse<T> {
private T data;
private Map<String, List<String>> errors;
public ApiResponse(T data, Map<String, List<String>> errors) {
this.data = data;
this.errors = errors;
}
}
Where T can be any class.
I tried implementing a JsonDeserializer<ApiResponse<T>> based on some examples I found around the internet but I can't wrap my head around how to make it work as much automatically as possible and let Retrofit and Gson do the heavy lifting
My Converter class is as follows:
public class ApiResponseDeserializer<T> implements JsonDeserializer<ApiResponse<T>> {
private Class clazz;
public ApiResponseDeserializer(Class clazz) {
this.clazz = clazz;
}
#Override
public ApiResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final JsonObject body = jsonObject.getAsJsonObject("body");
final JsonObject errors = body.getAsJsonObject("errors");
final JsonObject data = body.getAsJsonObject("data");
Map<String, List<String>> parsedErrors = new HashMap<>();
for(String key : errors.keySet()) {
List<String> errorsList = new ArrayList<>();
JsonArray value = errors.getAsJsonArray(key);
Iterator<JsonElement> valuesIterator = value.iterator();
while(valuesIterator.hasNext()) {
String error = valuesIterator.next().getAsString();
errorsList.add(error);
}
parsedErrors.put(key, errorsList);
}
T parsedData = context.deserialize(data, clazz);
return new ApiResponse<T>(parsedData, parsedErrors);
}
}
and then when building my retrofit client
public static Retrofit getClient() {
if (okHttpClient == null) {
initOkHttp();
}
Gson gson = new GsonBuilder()
.registerTypeAdapter(ApiResponse.class, new ApiResponseDeserializer<>(......) // PROBLEM
.create();
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(Const.API_BASE_URL)
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}
return retrofit;
}
But I feel like it's not generic enough to be able to convert my classes automatically. And also I have no idea how should I hint Gson what type my data.
My endpoints are defined as follows:
#POST("users/signup")
Single<ApiResponse<RegisterResponseData>> register(#Body RegisterRequest request);
But how do I make a generic Retrofit instance with a generic Gson type adapter that knows how to convert my response to a ApiResponse<RegisterResponseData>? And knows that the data property from the response should be converted to an object of type RegisterResponseData...
When you specify return type in Retrofit's client it's passed to Retrofit's converter as Type and then Gson receives that type which will be your ApiResponse<RegisterResponseData>. From that point Gson will understand that data is of type RegisterResponseData and will produce your model object.
Just try it without your ApiResponseDeserializer and you'll see it's working.
Edit:
Answering your additional question in comments:
If you want to skip your "body" object in json you can write your wrapper object like this:
public class ApiResponse<T> {
#SerializedName("body")
private ApiResponseBody<T> body;
public ApiResponse() {
}
public ApiResponse(ApiData<T> body) {
this.body = body;
}
}
public class ApiResponseBody<T> {
#SerializedName("data")
private T data;
#SerializedName("errors")
private Map<String, List<String>> errors;
public ApiResponseBody() {
}
public ApiResponseBody(T data, Map<String, List<String>> errors) {
this.data = data;
this.errors = errors;
}
}
And use it in usual way
#POST("users/signup")
Single<ApiResponse<RegisterResponseData>> register(#Body RegisterRequest request);
is there any easy way to to convert this json:
{
...,
"pictures": [
"url1",
"url2"
],
...
}
to
List<Picture> pictures
where Picture is:
class Picture{
String url;
}
It won't work as above, because I have an exception, saying
Expected BEGIN_OBJECT but was STRING
You need to implement a custom deserializer for this.
Should be looking like this (I didn't try to execute, but that should give you the idea where to start, presumably you have a public constructor with String argument in your Picture.class)
private class PictureDeserializer implements JsonDeserializer<Picture> {
public Picture deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
return new Picture(json.getAsJsonPrimitive().getAsString());
}
}
It should be registered:
GsonBuilder gson = new GsonBuilder();
gson.registerTypeAdapter(Picture.class, new PictureDeserializer());
For example
public class HistoryRecordDeserializer implements JsonDeserializer<HistoryRecord> {
private LocalDateTimeConverter dateTimeConverter = new LocalDateTimeConverter();
#Override
public HistoryRecord deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
User user = new User();
user.setId(UUUID.fromString(json.get("user").get("id").getAsString()));
OtherData data = new OtherData();
data.setData(json.get("otherData").getAsLong());
return UserAndData(user, otherData);
}
As you can see, I instantiate User and OtherData manually, but I think there is a better solution. What is the best way to deserialize user with fromJson(...)? Should I pass Gson instance to HistoryRecordDeserializer? Should I create new one?
My problem was solved by using JsonDeserializationContext.
#Override
public HistoryRecord deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject object = json.getAsJsonObject();
JsonObject extras = object.get("extraData").getAsJsonObject();
HistoryRecord hr = object.context.deserialize(object.get("data"), HistoryRecord.class);
hr.appendExtraData(extras, HistoryRecordExtraData.class);
...
}
As #varren sad:
If you Gson can deserialize this, then context will be also able to do this.
So, you can even apply another custom type adapter(LocalDateTimeConverter):
gson = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeConverter())
.registerTypeHierarchyAdapter(HistoryRecord.class, new HistoryRecordDeserializer())
.create();
and use it inside HistoryRecordDeserializer:
LocalDateTime localDateTime = context.deserialize(object.get("dateTime"), LocalDateTime.class);
I have one class User, I received JSON (for User class) from system1 and I should read info , validate then forward to system2, I can't touch these 2 systems, the problem is the names of keys are different, I want to differentiate between deserialized and serialized name
received JSON is :
{"userId":"user1","pwd":"123456","country":"US"}
"{"username":"user1","password":"123456","country":"US"}"
But the sent should be like this
I am using Gson lib, and this is my code:
User class:
class User implements Cloneable {
#SerializedName("username")
private String username ;
#SerializedName("password")
private String password ;
#SerializedName("country")
private String country ;
}
TestJson class
class TestJson {
private static GsonBuilder gsonBuilder;
private static Gson gson;
public static Object fromJson(String json, Class clz) {
gson = new Gson();
return gson.fromJson(json, clz);
}
public static String toJson(Object obj) {
gsonBuilder = new GsonBuilder();
gson = gsonBuilder.create();
String json = gson.toJson(obj);
return json;
}
public static void main(String[] args) {
String json2 = "{\"userId\":\"user1\",\"pwd\":\"123456\",\"country\":\"US\"}";
User user = (User) TestJson.fromJson(json2, User.class);
System.out.println(user.getPassword());
User u = new User("user1","123456","US");
String json1 = TestJson.toJson(u);
System.out.println(json1);
}
}
If there are alternative names of field just use alternate param of #SerializedName
public class User {
#SerializedName(value="username", alternate={"userId", "useriD"})
private String username ;
...
}
You can create custom serializer/deserializer for this purpose.
Serializer:
public class UserSerializer implements JsonSerializer<User> {
#Override public JsonElement serialize(User obj, Type type, JsonSerializationContext jsonSerializationContext) {
..........
}
}
Deserializer:
public class UserDeserializer implements JsonDeserializer<User> {
#Override public User deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
...........
}
}
and to create Gson instance:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(User.class, new UserSerializer());
gsonBuilder.registerTypeAdapter(User.class, new UserDeserializer());
Gson gson = gsonBuilder.create();
Example
Edit: this is an example of a custom deserializer which might fit into your need. We don't need a custom serializer in this case.
Add this UserDeserializer.java:
public class UserDeserializer implements JsonDeserializer<User> {
#Override
public User deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
JsonObject obj = json.getAsJsonObject();
User user = new User(obj.get("userId").getAsString(), obj.get("pwd").getAsString(), obj.get("country").getAsString());
return user;
}
}
Replace your fromJson implementation with this (I use generic to avoid the need for casting when calling fromJson):
public static <T> T fromJson(String json, Class<T> clz) {
gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(User.class, new UserDeserializer());
gson = gsonBuilder.create();
return gson.fromJson(json, clz);
}
The only way I can think of would be to have a custom Adapter or deser to a JsonObject and then map it to your User.
With Genson you can create two instances of Genson, one for deserialization and another one for serializaiton. The one used in deserialization could be configured with renamed properties like that.
// you can also precise that you want to rename only the properties from User class
Genson genson = new GensonBuilder()
.rename("username", "userId")
.rename("password", "pwd")
.create();
User user = genson.deserialize(json, User.class);