I am attempting to de-serialise some JSON from Wikipedia using the GSON library. Typically, I would create each object in Java as they are in JSON, as example:
{
query: {
pages: {
page: {
pageid: 168079,
ns: 0,
title: "2010 FIFA World Cup"
}
}
}
}
This would allow me to de-serialise using the following Java code:
class PageIdResponseGson
{
private QueryResults query;
public static class QueryResults
{
private PageResults pages;
public PageResults getPages() {
return pages;
}
}
public static class PageResults
{
private PageResult page;
public PageResult getPage() {
return page;
}
}
public static class PageResult
{
private int pageid;
private int ns;
private String title;
public int getNs() {
return ns;
}
public int getPageid() {
return pageid;
}
public String getTitle() {
return title;
}
}
public QueryResults getQuery() {
return query;
}
}
However the Wikipedia JSON uses the page-id as the object name, which means I can't take this approach.
{
query: {
pages: {
168079: {
pageid: 168079,
ns: 0,
title: "2010 FIFA World Cup"
}
}
}
}
Is there a way I can use some kind of generic wrapper to handle these dynamic object names?
You have several alternatives here:
You use Hashmaps-->Anything can be a key in a hashmap and GSON will do just fine.
You implement your own TypeAdapter and TypeAdapterFactory to handle such cases and when you meet a field that is a number that does not exists in your target class then you know you have your pageId and it should be remapped on your PageResults.page member.
Related
I am using spring-boot along with Hateoas. One of my API exposes hateoas links as a collection "_links":[ instead if an object "_links":{. I am not sure why it is using array notation instead of an object. Please find the code below. Any help would be appreciated.
public class Book {
private String id;
private BookInfo bookInfo;
}
public class BookInfo extends ResourceSupport{
private String bookUid;
private String bookName;
private String authhorName;
private String bookGenre;
#Override
#JsonProperty("_links")
#JsonInclude(JsonInclude.Include.NON_NULL)
public List<Link> getLinks() {
return super.getLinks();
}
}
#RestController
#RequestMapping(value = "/api/v1/", produces = APP_JSON)
public class BookController {
#GetMapping("getBooks")
public ResponseEntity<Book> getTransactionStatus() {
Book book = bookRepo.getAllBooks();
book.getBookInfo().add(addLinks(book.getId()));
return ResponseEntity.ok().contentType(MediaType.valueOf(APP_JSON)).body(book);
}
public SuperLink getBookInfoLinks(String bookUid) {
return new SuperLink(
linkTo(methodOn(BookController.class).getBook(bookUid))
.withRel("retrieve-book").expand(),APP_JSON);
}
}
public class SuperLink extends Link {
#XmlAttribute
#JsonInclude(JsonInclude.Include.NON_NULL)
private String accepts;
public SuperLink(Link link) {
super(link.getHref(), link.getRel());
}
public SuperLink(Link link, String accepts) {
super(link.getHref(), link.getRel());
this.accepts = accepts;
}
public String getAccepts() {
return accepts;
}
public void setAccepts(String accepts) {
this.accepts = accepts;
}
}
Actual output
{
"id":"bookId",
"BookInfo":{
"bookUid":"bookUid",
"_links":[
{
"rel":"retrieve-book",
"href":"http://localhost/api/v1/book/bookId",
"accepts":"application/json"
}
]
}
}
Expected output
{
"id":"bookId",
"BookInfo":{
"bookUid":"bookUid",
"_links":
{
"retrieve-book": {
"href":"http://localhost/api/v1/book/bookId",
"accepts":"application/json"
}
}
}
}
This is happening because you are using List in you code.
#Override
#JsonProperty("_links")
#JsonInclude(JsonInclude.Include.NON_NULL)
public List<Link> getLinks() {
return super.getLinks();
}
You should use Link object instead of List of Link.
The links should be serialized as a map, not as a list. You either convert it into a map yourself or you can use custom serializer/deseralizer for that. Fortunately Spring already has them:
#Override
#JsonProperty("_links")
#JsonInclude(Include.NON_EMPTY)
#JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class)
#JsonDeserialize(using = Jackson2HalModule.HalLinkListDeserializer.class)
public List<Link> getLinks() {
return super.getLinks();
}
--- edit
In order to make it work you will need the halJacksonHttpMessageConverter bean in the list of message-converters. Create a WebMvcConfigurer and add the halJacksonHttpMessageConverter to the converters in the extendMessageConverters method.
#Autowired
private HttpMessageConverter halJacksonHttpMessageConverter;
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(halJacksonHttpMessageConverter);
}
You should add it to the front of the list, or remove the original jacksonHttpMessageConverter from the list.
I try to send the json object to rest services but I get some error like this:
http://localhost:8080/api/v1/cardLimit 400 (Bad Request);
Wrap to JSON
public class GameLimit implements Serializable {
private static final long serialVersionUID = 1L;
private LimitType firstLimit;
private LimitType secondLimit;
public LimitType getFirstLimit() {
return firstLimit;
}
public void setFirstLimit(LimitType firstLimit) {
this.firstLimit = firstLimit;
}
public LimitType getSecondLimit() {
return secondLimit;
}
public void setSecondLimit(LimitType secondLimit) {
this.secondLimit = secondLimit;
}
}
public class LimitType implements Serializable{
private static final long serialVersionUID = 1L;
private BigDecimal limit;
private String type;
private String status;
public BigDecimal getLimit() {
return limit;
}
public void setLimit(BigDecimal limit) {
this.limit = limit;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
Limit request object
public class LimitReq extends GameLimit {
private String key;
public String getKey() {
return key;
}
}
RestController:
#RequestMapping(value = "/GameLimit", method = RequestMethod.POST)
public Response setCardLimit(#RequestBody GameLimitReq request) throws Exception {
return limitService.updateGameLimit(request);
}
TypeScript client:
changeLimits(firstLimit: IWidgetLimit, secondLimit: IWidgetLimit, key: string): ng.IPromise<any> {
return this.$http.post(this.apiPrefix + '/GameLimit', {
'firstLimit': {
limit: firstLimit.limit,
type: firstLimit.type,
status: firstLimit.status
},
'secondLimit': {
limit: secondLimit.limit,
type: secondLimit.type,
status: secondLimit.status,
},
key: key
}).then(function (response: any) {
return response.data;
}.bind(this));
}
Seeing this question and answer a 400 error indicates that your json is malformed.
From your code snippets, it seems that the line limitService.updateGameLimit(request); should provide the JSON, yet it is not included in the code snipets. Once you have the output of that method, you can run it through JsonLint to check the syntax. Then repairs can be made from there.
From your typescript client it seems that this is supplying invalid json. While I am not totally versed in typescript this certainly has some malformed JSON even if there are implied quotes. At the very least there should be double quotes around firstLimit, secondLimit, and key.
It's because your json is not being formed properly.
And there are multiple reasons for that
Your Keys are supposed to be strings, and wrapped in quotes. eg: use "type" instead of type.
You have a comma at the end of the line
status: secondLimit.status,
Remove that comma.
After you are done with it, validate a sample output on jsonlint.com or a similar service. It will help you figure out errors.
When written by hand, the $project step in my aggregation pipeline looks like:
{
"$project":{
"DRIVE":{
"componentSummary":{"manufacturer" : "$_id.DRIVE_manufacturer"},
"componentCount":"$_id.DRIVE_componentCount"
},
"hostnames":1,
"_id":0
}
}
I understand that I can use the ProjectionOperationBulder to create a single level of nesting (using builder.nested), to make something like, say:
{
"$project":{
"DRIVE":{
"manufacturer":"$_id.DRIVE_manufacturer"
},
"hostnames":1,
"_id":0
}
}
But I can't seem to figure out how to nest another level deep, as the Field interface only allows for a String name and a String target, rather than being able to define antother Field as the target.
Thanks!
For anyone else struggling with this -- Spring Data Mongo does not natively support multi level nesting as of this writing (stable version 1.9.5). However, as of 1.9.3, it does support custom AggregationExpressions that allow you to define the behavior yourself. Be aware that if you go down this route, you'll have to build the JSON for the query mostly by hand. My implementation is pretty quick and dirty but here it is for reference's sake.
protected class NestedField implements Field {
private String name;
private List<Field> fields;
public NestedField(String name, List<Field> fields) {
this.name = name;
this.fields = fields;
}
public List<Field> getFields() {
return fields;
}
#Override
public String getName() {
return name;
}
private String escapeSystemVariables(String fieldTarget) {
if (fieldTarget.startsWith("_id")) {
return StringUtils.prependIfMissing(fieldTarget, "$");
} else {
return fieldTarget;
}
}
private String encloseStringInQuotations(String quotable) {
return JSON.serialize(quotable);
}
private String buildSingleFieldTarget(Field field) {
if (field instanceof NestedField) {
return String.join(":", encloseStringInQuotations(field.getName()), field.getTarget());
}
return String.join(":", encloseStringInQuotations(field.getName()), encloseStringInQuotations(escapeSystemVariables(
field.getTarget())));
}
private String buildFieldTargetList(List<Field> fields) {
List<String> fieldStrings = new ArrayList<>();
fields.forEach(field -> {
fieldStrings.add(buildSingleFieldTarget(field));
});
return Joiner.on(",").skipNulls().join(fieldStrings);
}
#Override
public String getTarget() {
// TODO Auto-generated method stub
return String.format("{%s}", buildFieldTargetList(fields));
}
#Override
public boolean isAliased() {
return true;
}
}
protected class NestedProjection implements AggregationExpression {
private List<Field> projectedFields;
public NestedProjection(List<Field> projectedFields) {
this.projectedFields = projectedFields;
}#Override
public DBObject toDbObject(AggregationOperationContext context) {
DBObject projectionExpression = new BasicDBObject();
for(Field f : projectedFields) {
//this is necessary because if we just put f.getTarget(), spring-mongo will attempt to JSON-escape the string
DBObject target = (DBObject) com.mongodb.util.JSON.parse(f.getTarget());
projectionExpression.put(f.getName(), target);
}
return projectionExpression;
}
}
I'm trying to deserialize a JSON which containes a String and a list of objects in my Spring web application.
JSON
[
{
"jsonrpc":"2.0",
"result":[
{
"event":{
"id":"27809810",
"name":"Spezia v Trapani",
"countryCode":"IT",
"timezone":"Europe/London",
"openDate":"2016-05-28T16:30:00.000Z"
},
"marketCount":13
},
{
"event":{
"id":"27811083",
"name":"Torino U19 v Atalanta U19",
"countryCode":"IT",
"timezone":"Europe/London",
"openDate":"2016-05-29T16:15:00.000Z"
},
"marketCount":18
},
...
]
My classes are:
ListEventsResponse class
public class ListEventsResponse {
private String jsonrpc;
private List<ListEventsResult> result;
public ListEventsResponse() { }
public String getJsonrpc() {
return jsonrpc;
}
public void setJsonrpc(String jsonrpc) {
this.jsonrpc = jsonrpc;
}
public List<ListEventsResult> getResult() {
return result;
}
public void setResult(List<ListEventsResult> result) {
this.result = result;
}
}
ListEventsResult class
public class ListEventsResult {
private Event event;
private int marketCount;
public ListEventsResult() { }
public Event getEvent() {
return event;
}
public void setEvent(Event event) {
this.event = event;
}
public int getMarketCount() {
return marketCount;
}
public void setMarketCount(int marketCount) {
this.marketCount = marketCount;
}
}
I have also Event class, composed by 5 String (id, name, etc.).
Controller
[...]
ListEventsResponse listEvents = new Gson().fromJson(response.toString(), ListEventsResponse.class);
List<ListEventsResult> eventsList = listEvents.getResult();
return new ModelAndView("page", "eventsList", eventsList);
My .jsp page
[...]
<c:forEach items="${eventsList}" var="listEventsResult">
Match: <c:out value="${listEventsResult.name}"/>
</c:forEach>
[...]
My code runs and doesn't give any error, but no match is shown on my page, in fact listEvents doesn't contains any object.
I can't understand how to deserialize properly the list of objects, so my question is: which logic is behind the deserialization of a json which contains a list of objects?
I post my code just to explain better my problem.
As you have a Json Array as response , you need to deserialize like below
Gson gson = new Gson();
Type type = new TypeToken<List<ListEventsResponse>>(){}.getType();
List<ListEventsResponse> events = (List<ListEventsResponse>) gson.fromJson(response.toString(), type);
I have a JSON object that looks like this
{
id:int,
tags: [
"string",
"string"
],
images: {
waveform_l:"url_to_image",
waveform_m:"url_to_image",
spectral_m:"url_to_image",
spectral_l:"url_to_image"
}
}
I'm trying to use retrofit to parse the JSON and create the interface. The problem that I have is that I get a null for the images urls. Everything else works, I am able to retrieve the id, the tags, but when I try to get the images they are all null.
I have a sound pojo that looks like this:
public class Sound {
private Integer id;
private List<String> tags = new ArrayList<String>();
private Images images;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Images getImages() {
return images;
}
public void setImages(Images images) {
this.images = images;
}
... setters and getter for tags as well
}
and I have a Images pojo that looks like this:
public class Images {
private String waveformL;
private String waveformM;
private String spectralM;
private String spectralL;
public String getWaveformL() {
return waveformL;
}
public void setWaveformL(String waveformL) {
this.waveformL = waveformL;
}
public String getWaveformM() {
return waveformM;
}
public void setWaveformM(String waveformM) {
this.waveformM = waveformM;
}
public String getSpectralM() {
return spectralM;
}
public void setSpectralM(String spectralM) {
this.spectralM = spectralM;
}
public String getSpectralL() {
return spectralL;
}
public void setSpectralL(String spectralL) {
this.spectralL = spectralL;
}
}
Whenever I try to call images.getWaveformM() it gives me a null pointer. Any ideas?
#SerializedName can also be used to solve this. It allows you to match the expected JSON format without having to declare your Class variable exactly the same way.
public class Images {
#SerializedName("waveform_l")
private String waveformL;
#SerializedName("waveform_m")
private String waveformM;
#SerializedName("spectral_m")
private String spectralM;
#SerializedName("spectral_l")
private String spectralL;
...
}
If the only differences from the JSON to your class variables are the snake/camel case then perhaps #njzk2 answer works better but in cases where there's more differences outside those bounds then #SerializeName can be your friend.
You possibly need this part:
Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) will allow gson to automatically transform the snake case into camel case.
public class Images {
private String waveform_l;
private String waveform_m;
private String spectral_m;
private String spectral_m;
}
Key name should be same in model as in json other wise it won't recognise it else you haven't define it at GsonBuilder creation.Generate the getter setter for the same and you will be good to go