Reputation: 6063
Good day all. I am having a bit of trouble understanding this. I have a JSON that looks like this:
{
"data": [
{
"id": "43",
"type": "position",
"attributes": {
"address-id": "1",
"employer-id": "11"
}
}
],
"included": [
{
"id": "1",
"type": "address",
"attributes": {
"line-1": "21 london london",
"line-2": "",
"line-3": "",
"locality": "",
"region": "London",
"post-code": "",
"country": "UK",
"latitude": "",
"longitude": ""
}
},
{
"id": "11",
"type": "employer",
"attributes": {
"title": "Mr",
"first-name": "S",
"last-name": "T"
}
}
]
}
And my Retrofit call is:
@GET("/api/positions")
Single<PositionResponse> getPosition();
And my PositionResponse
class:
public class PositionResponse {
@SerializedName("data")
@Expose
private List<DataResponse> data;
@SerializedName("included")
@Expose
private List<IncludedModel> included;
public List<DataResponse> getData() {
return data;
}
public void setData(List<DataResponse> data) {
this.data = data;
}
public List<IncludedModel> getIncluded() {
return included;
}
public void setIncluded(List<IncludedModel> included) {
this.included = included;
}
}
}
Now imagine it has a lot more data. How can I create a custom TypeAdapter
or JsonDeserializer
for parsing the List<IncludedModel>
? For some reason, I can create a custom JsonDeserializer
or TypeAdapter
for Object
, but when it comes to a List
, I don't seem to be able to get that to work.
My TypeAdapter
is as follows:
public class IncludedTypeAdapter extends TypeAdapter<ArrayList<IncludedModel>> {
@Override
public void write(JsonWriter out, ArrayList<IncludedModel> value) throws IOException {
}
@Override
public ArrayList<IncludedModel> read(JsonReader in) throws IOException {
ArrayList<IncludedModel> list = new ArrayList<>();
IncludedModel model = new IncludedModel();
Gson gson = new Gson();
in.beginArray();
String id = null;
//in.beginObject();
while(in.hasNext()){
JsonToken nextToken = in.peek();
if(JsonToken.BEGIN_OBJECT.equals(nextToken)){
in.beginObject();
} else if(JsonToken.NAME.equals(nextToken)){
if(JsonToken.NAME.name().equals("id")){
id = in.nextString();
model.setId(id);
} else if(JsonToken.NAME.name().equals("type")){
String type = in.nextString();
model.setMytype(type);
switch (type) {
case BaseModelType.Employer:
EmployerResponse employer = gson.fromJson(in, EmployerResponse.class);
model.setEmployer(employer);
break;
}
}
}
}
list.add(model);
return list;
}
And i register to my Gson:
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(IncludeModel.class, new IncludedTypeAdapter());
//gsonBuilder.registerTypeAdapter(new IncludedTypeAdapter());
gsonBuilder.serializeNulls();
Gson gson = gsonBuilder.create();
return gson;
Which I register on retrofit through GsonConverterFactory
.
I am getting:
Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 6292 path $.included[0]
which I suspect is because my Retrofit response is <PositionResponse>
which is a JsonObject
.
To summarize my question: how do I deserialize the List<IncludeModel>
object with my own custom type adapter bearing in mind the response type from my Retrofit service is PositionResponse
? Many thanks for your patients and answers.
Upvotes: 0
Views: 1442
Reputation: 21105
It's easy if you're using JSON tree models using JsonDeserializer
. Pure type adapters are somewhat an overkill (as well as RuntimeTypeAdapterFactory is, I think, since it's still tree-oriented), and in the most simple case for your JSON document you could use something like this (you can find a similar approach in my yesterday answer having some more explanations, but you case slightly differs).
I'm assuming you would like to have mappings like these:
abstract class Element {
final String id = null;
private Element() {
}
static final class Address
extends Element {
@SerializedName("line-1") final String line1 = null;
@SerializedName("line-2") final String line2 = null;
@SerializedName("line-3") final String line3 = null;
@SerializedName("locality") final String locality = null;
@SerializedName("region") final String region = null;
@SerializedName("post-code") final String postCode = null;
@SerializedName("country") final String country = null;
@SerializedName("latitude") final String latitude = null;
@SerializedName("longitude") final String longitude = null;
private Address() {
}
@Override
public String toString() {
return country + " " + region;
}
}
static final class Employer
extends Element {
@SerializedName("title") final String title = null;
@SerializedName("first-name") final String firstName = null;
@SerializedName("last-name") final String lastName = null;
private Employer() {
}
@Override
public String toString() {
return title + ' ' + firstName + ' ' + lastName;
}
}
static final class Position
extends Element {
@SerializedName("address-id") final String addressId = null;
@SerializedName("employer-id") final String employerId = null;
private Position() {
}
@Override
public String toString() {
return '(' + addressId + ';' + employerId + ')';
}
}
}
All you have to do is just:
Gson
once again losing the original configuration; you redo all Gson can do out of box: lists and POJO by reflection; JsonToken
are much better if checked via switch
(by the way enum
s are singletons and it's perfectly legal to compare them using reference equality ==
), etc).So, it can be implemented by something like this:
final class ElementJsonDeserializer
implements JsonDeserializer<Element> {
private static final JsonDeserializer<Element> elementJsonDeserializer = new ElementJsonDeserializer();
private ElementJsonDeserializer() {
}
static JsonDeserializer<Element> getElementJsonDeserializer() {
return elementJsonDeserializer;
}
@Override
public Element deserialize(final JsonElement jsonElement, final Type type, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = jsonElement.getAsJsonObject();
final String typeCode = jsonObject.getAsJsonPrimitive("type").getAsString();
final Class<? extends Element> clazz;
switch ( typeCode ) {
case "address":
clazz = Element.Address.class;
break;
case "employer":
clazz = Element.Employer.class;
break;
case "position":
clazz = Element.Position.class;
break;
default:
throw new JsonParseException("Unrecognized type: " + typeCode);
}
reattach(jsonObject, "attributes");
return context.deserialize(jsonElement, clazz);
}
private static void reattach(final JsonObject parent, final String property) {
final JsonObject child = parent.getAsJsonObject(property);
parent.remove(property); // remove after we're sure it's a JSON object
copyTo(parent, child);
}
private static void copyTo(final JsonObject to, final JsonObject from) {
for ( final Entry<String, JsonElement> e : from.entrySet() ) {
to.add(e.getKey(), e.getValue());
}
}
}
Of course, you can refactor the above to extract a strategy to implement the strategy design pattern to reuse it. Put it all together:
final class Response {
final List<Element> data = null;
final List<Element> included = null;
}
(The above one looks like a Map<String, List<Element>>
but you decide).
private static final Gson gson = new GsonBuilder()
.registerTypeAdapter(Element.class, getElementJsonDeserializer())
.create();
public static void main(final String... args)
throws IOException {
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43811168.class, "data.json") ) {
final Response response = gson.fromJson(jsonReader, Response.class);
dump(response.data);
dump(response.included);
}
}
private static void dump(final Iterable<Element> elements) {
for ( final Element e : elements ) {
System.out.print(e.getClass().getSimpleName());
System.out.print(" #");
System.out.print(e.id);
System.out.print(": ");
System.out.println(e);
}
}
Output:
Position #43: (1;11)
Address #1: UK London
Employer #11: Mr S T
Upvotes: 1