I have a problem with parsing my custom response because the I have a response with Localization properties.
I am recieving a response that looks something like this:
[
{
"id": "dummyID1",
"name.en_US": "dummyNameEn1",
"name.fi_FI": "dummyNameFi1"
},
{
"id": "dummyID2",
"name.en_US": "dummyNameEn2",
"name.fi_FI": "dummyNameFi2"
},
{
"id": "dummyID3",
"name.en_US": "dummyNameEn3",
"name.fi_FI": "dummyNameFi3"
}...
]
And to parse that I have created a custom class Device.java:
public class Device {
public String id;
public LocalizedString name;
public Device(String id, LocalizedString name) {
this.id = id;
this.name = name;
}
//Getters and setters
}
Now here we have a custom object named LocalizedString.java:
public class LocalizedString implements Parcelable {
public static final Creator<LocalizedString> CREATOR = new Creator<LocalizedString>() {
#Override
public LocalizedString createFromParcel(Parcel in) {
return new LocalizedString(in);
}
#Override
public LocalizedString[] newArray(int size) {
return new LocalizedString[size];
}
};
private String en_US;
private String fi_FI;
public LocalizedString(String en, String fi) {
this.en_US = en;
this.fi_FI = fi;
}
protected LocalizedString(Parcel in) {
en_US = in.readString();
fi_FI = in.readString();
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(en_US);
dest.writeString(fi_FI);
}
//Getters, setters
}
Now in my response I want to create a list of Device's but it does not seem to understand how the ´LocalizedString´ works. Since my request is returning a <List<Device>> I cannot really customly parse it either.
Here is how I try to parse it:
Call<List<Device>> call = getMainActivity().getRestClient().getDevices();
call.enqueue(new Callback<List<Device>>() {
#Override
public void onResponse(Call<List<Device>> call, Response<List<Device>> response) {
if (isAttached()) {
if (response.isSuccessful()) {
// get data
List<Device> items = response.body();
}
}
}
#Override
public void onFailure(Call<List<Device>> call, Throwable t) {
if (isAttached()) {
Logger.debug(getClass().getName(), "Could not fetch installation document devices past orders", t);
getMainActivity().showError(R.string.error_network);
}
}
});
And:
#GET("document/devices")
Call<List<Device>> gettDevices();
What am I supposed to do in this situation to bind the name to the Device and later be able to either get en_US or fi_FI.
Better you can write it like this
public class Device {
#SerializedName("id")
public String id;
#SerializedName("name.en_US")
public String en;
#SerializedName("name.fi_FI")
public String fi;
public Device(String id, String english, String fi) {
this.id = id;
this.en = english;
this.fi = fi;
}
//Getters and setters
}
If you can control the source of the JSON, then a modification of that JSON structure is easy to solve your problem.
If you can not, the one way we can use to solve your problem is to use Jackson and custom deserializer:
public class DeviceDeserializer extends StdDeserializer<Device> {
public DeviceDeserializer() {
this(null);
}
public DeviceDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Device deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String id = getStringValue(node, "id");
String en = getStringValue(node, "name.en_EN");
String fi = getStringValue(node, "name.fi_FI");
LocalizedString localized = new LocalizedString(en, fi);
return new Device(id, localizedString);
}
private String getStringValue(JsonNode node, String key) {
// Throws exception or use null is up to you to decide
return Optional.ofNullable(node.get("id"))
.map(JsonNode::asText)
.orElse(null);
}
}
Manually register the deserializer yourself or using the annotation:
#JsonDeserialize(using = DeviceDeserializer.class)
public class Device {
...
Note that you must enable retrofit jackson converter plugin: (see the Retrofit Configuration part)
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(JacksonConverterFactory.create())
.build();
Read this: Get nested JSON object with GSON using retrofit
Related
I want to have several different schema when serializing with Jackson.
Suppose that I have the following classes:
public class Department {
private Person head;
private Person deputy;
private List<Person> staff;
// getters and setters
}
public class Person {
private String name
private int code;
// getters and setters
}
Now, I want to have two different schema for Department class. The first one contains only head and deputy where head includes both name and code, but deputy has only name. The second schema should include all fields recursively.
Thus, we will have two different jsons. With the first schema:
{
"head" : {
"name" : "John",
"code" : 123
},
"deputy" : {
"name" : "Jack"
}
}
, and with the second schema:
{
"head" : {
"name" : "John",
"code" : 123
},
"deputy" : {
"name" : "Jack",
"code" : "234"
},
"staff": [
{
"name" : "Tom",
"code" : "345"
},
{
"name" : "Matt",
"code" : "456"
},
]
}
QUESTION: How should I do it with Jackson?
NOTE: These classes are just examples. For this simple example, writing four different wrapper classes may be possible but think about a complex example with dozen of classes that each one has several fields. Using wrapper classes, we should generate a lot of boilerplate code.
Any help would be appreciated!
Although the #bigbounty solution is excellent and I think that Views, in addition to specific DTOs, are the way to go in general, in this case it may not be applicable because, for the same class Person we actually need two different behaviors in the same view.
#JsonFilters can be used to solve the problem.
This main method computes the graph what you need:
public static void main(String[] args) throws JsonProcessingException {
final PropertyFilter departmentFilter = new SimpleBeanPropertyFilter() {
#Override
public void serializeAsField
(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if (include(writer)) {
final String name = writer.getName();
if (!name.equals("deputy") && !name.equals("staff")) {
writer.serializeAsField(pojo, jgen, provider);
return;
}
if (name.equals("staff")) {
return;
}
// Ideally it should not be muted.
final Department department = (Department)pojo;
final Person deputy = department.getDeputy();
deputy.setCode(-1);
writer.serializeAsField(department, jgen, provider);
} else if (!jgen.canOmitFields()) { // since 2.3
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
};
final PropertyFilter personFilter = new SimpleBeanPropertyFilter() {
#Override
public void serializeAsField
(Object pojo, JsonGenerator jgen, SerializerProvider provider, PropertyWriter writer)
throws Exception {
if (include(writer)) {
if (!writer.getName().equals("code")) {
writer.serializeAsField(pojo, jgen, provider);
return;
}
int code = ((Person) pojo).getCode();
if (code >= 0) {
writer.serializeAsField(pojo, jgen, provider);
}
} else if (!jgen.canOmitFields()) { // since 2.3
writer.serializeAsOmittedField(pojo, jgen, provider);
}
}
#Override
protected boolean include(BeanPropertyWriter writer) {
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
return true;
}
};
final Department department = new Department();
final Person head = new Person("John", 123);
final Person deputy = new Person("Jack", 234);
final List<Person> personList = Arrays.asList(new Person("Tom", 345), new Person("Matt", 456));
department.setHead(head);
department.setDeputy(deputy);
department.setStaff(personList);
final ObjectMapper mapper = new ObjectMapper();
final FilterProvider schema1Filters = new SimpleFilterProvider()
.addFilter("deparmentFilter", departmentFilter)
.addFilter("personFilter", personFilter)
;
mapper.setFilterProvider(schema1Filters);
final String withSchema1Filters = mapper.writeValueAsString(department);
System.out.printf("Schema 1:\n%s\n", withSchema1Filters);
// You must maintain the filters once the classes are annotated with #JsonFilter
// We can use two no-op builtin filters
final FilterProvider schema2Filters = new SimpleFilterProvider()
.addFilter("deparmentFilter", SimpleBeanPropertyFilter.serializeAll())
.addFilter("personFilter", SimpleBeanPropertyFilter.serializeAll())
;
mapper.setFilterProvider(schema2Filters);
final String withSchema2Filters = mapper.writeValueAsString(department);
System.out.printf("Schema 2:\n%s\n", withSchema2Filters);
}
For this code to work, you must annotate the Department class with:
#JsonFilter("deparmentFilter")
And the Person class with:
#JsonFilter("personFilter")
As you can see, Jackson also provides several builtin filters.
This code is very coupled to the test classes that you proposed but it can be extended in ways that makes it more generic.
Please, take a look at SimpleBeanPropertyFilter for examples of how to create your own filter.
There is a feature JsonViews in the Jackson library.
You need to have a class for the views
public class Views {
public static class Normal{}
public static class Extended extends Normal{}
}
Next you annotate the Department class
import com.fasterxml.jackson.annotation.JsonView;
import java.util.List;
public class Department {
#JsonView(Views.Normal.class)
private Person head;
#JsonView(Views.Normal.class)
private Person deputy;
#JsonView(Views.Extended.class)
private List<Person> staff;
public Person getHead() {
return head;
}
public void setHead(Person head) {
this.head = head;
}
public Person getDeputy() {
return deputy;
}
public void setDeputy(Person deputy) {
this.deputy = deputy;
}
public List<Person> getStaff() {
return staff;
}
public void setStaff(List<Person> staff) {
this.staff = staff;
}
}
Keep the Person class as it is
public class Person {
private String name;
private int code;
public Person(String name, int code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
In the main function, where you are serializing the object, you need to enable the corresponding views.
public class Main {
static Department createDepartment(){
Department department = new Department();
Person head = new Person("John", 123);
Person deputy = new Person("Jack", 234);
List<Person> personList = Arrays.asList(new Person("Tom", 345), new Person("Matt", 456));
department.setHead(head);
department.setDeputy(deputy);
department.setStaff(personList);
return department;
}
public static void main(String[] args) throws JsonProcessingException {
Department department = createDepartment();
ObjectMapper mapper = new ObjectMapper();
String normal = mapper.writerWithView(Views.Normal.class).writeValueAsString(department);
String extended = mapper.writerWithView(Views.Extended.class).writeValueAsString(department);
System.out.println("Normal View - " + normal);
System.out.println("Extended View - " + extended);
}
}
The output is as follows:
Normal View - {"head":{"name":"John","code":123},"deputy":{"name":"Jack","code":234}}
Extended View - {"head":{"name":"John","code":123},"deputy":{"name":"Jack","code":234},"staff":[{"name":"Tom","code":345},{"name":"Matt","code":456}]}
Input Json file looks like below code:
{
"Payload": {
"any0": {
"pricingResponse": {
"priceDetails": {
"priceDetails": {
"seqId": "8015B000000Umo1QAC-",
"companyCode": "EPA",
"priceType": "Fuel Price",
"priceRequestSource": ""
},
"priceLineItems": {
"lineItem": [
{
"lineItemInfo": {
"seqId": "8015B000000Umo1QAC-28052018105047-022596",
"orderId": "00000266",
"lineItemId": "44",
"itemId": "70051",
"quantity": "100.00",
"unitPrice": "2.03200",
"unitPriceLCY": "2.03200"
}
}
]
}
}
}
}
}
}
Expected output(java class)
public class Payload
{
public Any0 any0;
}
/* Stub Class for Any0 */
public class Any0
{
public PricingResponse pricingResponse;
}
/* Stub Class for PricingResponse */
public class PricingResponse
{
public PriceDetails priceDetails;
}
/* Stub Class for PriceDetails */
public class PriceDetails
{
public String seqId;
public String priceRequestSource;
public String companyCode;
public String priceType;
public String deliveryType;
public String currencyCode;
public String priceDate;
public String applyOrderQuantity;
public String totalOrderQuantity;
public String customerId;
public String shipToId;
public String supplyLocationId;
public String transporterId;
public String onRun;
public String orderId;
public PriceDetails priceDetails;
public PriceLineItems priceLineItems;
public Error Error;
}
/* Stub Class for PriceLineItems */
public class PriceLineItems
{
public String orderId;
public List<LineItem> lineItem;
}
/* Stub Class for LineItem */
public class LineItem
{
public LineItemInfo lineItemInfo;
public AccountingDetails accountingDetails;
public Error Error;
}
I tried to convert it using Jackson Library it's working and creating as the separate POJO classes but I want to create nested class.This is my sample code
public void convert2JSON(URL inputJson, File outputPojoDirectory, String packageName, String className) throws IOException
{
JCodeModel codeModel = new JCodeModel();
URL source = inputJson;
GenerationConfig config = new DefaultGenerationConfig()
{
#Override
public boolean isGenerateBuilders()
{ // set config option by overriding method
return true;
}
public SourceType getSourceType()
{
return SourceType.JSON;
}
};
SchemaMapper mapper = new SchemaMapper(new RuleFactory(config, new Jackson2Annotator(config), new SchemaStore()), new SchemaGenerator());
mapper.generate(codeModel, className, packageName, source);
codeModel.build(outputPojoDirectory);
}
This is the output I am getting
Is there a possible way to convert dynamic nested Json class file into Java class? Any help would be appreciated.
Use a Map<String, ValueClass> for every structure that contains variable keys and unvarying values (of type ValueClass).
If you'd also like to deserialize maps with variable value structures, you'll need to write a custom deserializer that can infer which Java class should the JSON be deserialized to. Here's a tutorial for how can you do that with GSON.
I have problem with update object json after change on page where I get text. In below my code and response API.
Response API:
[
{ "title": "„Jak wykorzystać media i nowoczesne technologie w edukacji?” – warsztaty dla nauczycieli",
"url": "http://www.up.krakow.pl/uniwersytet/aktualnosci/1772-jak-wykorzystac-media-i-nowoczesne-technologie-w-edukacji-warsztaty-dla-nauczycieli"
}
]
Java Service:
public class NewsService implements NewsServiceInterface {
private Document doc = Jsoup.connect("http://www.up.krakow.pl/uniwersytet/aktualnosci").get();
private Elements links = doc.select("div.page-header");
private LinkedHashMap<String, String> store = new LinkedHashMap<>();
public NewsService() throws IOException {
}
#Override
public List<News> getNews() {
List<News> newsList = new ArrayList<>();
for (Element element : links) {
String title = element.select("a[href]").text(); // get only text
String url = "http://www.up.krakow.pl" + element.select("a[href]").attr("href"); // get only link
if (!store.containsKey(title)) {
store.put(title, url);
}
}
for (Map.Entry<String, String> entry : store.entrySet()) {
newsList.add(new News(entry.getKey(), entry.getValue()));
}
return Lists.reverse(newsList);
}
}
Java Controller:
public class NewsController {
private static final String API_CONTEXT = "/api/v1";
public NewsController(final NewsService newsService) {
get(API_CONTEXT + "/getnews", (request, response) -> {
return newsService.getNews();
}, json());
}
Java POJO:
public class News implements Serializable {
#Expose
#SerializedName("id")
private String id;
#Expose
#SerializedName("title")
private String title;
#Expose
#SerializedName("url")
private String url;
#Expose
#SerializedName("counterAllNews")
private String counterAllNews;
public News() {
}
public News(String title, String url) {
this.title = title;
this.url = url;
}
// getter and setter
}
Java Main:
public class Hello {
public static void main(String[] args) throws IOException {
try {
new NewsController(new NewsService());
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java JSON:
public class JsonUtil {
public static String toJson(Object object) {
return new Gson().toJson(object);
}
public static ResponseTransformer json() {
return JsonUtil::toJson;
}
}
Where is problem? The JSON is update if I restart jetty server. Otherwise not.
If I understand correctly, you always get the same results when repeatedly calling your service? And you expect to get changing entries because the original source where you fetch them changes?
This is because you read that information from www.up.krakow.pl/uniwersytet/aktualnosci only once when the NewsService is instantiated. And that is done only once in your main method:
new NewsController(new NewsService());
Change the NewsService implementation, so that you refetch the news data on every get:
public class NewsService implements NewsServiceInterface {
private LinkedHashMap<String, String> store = new LinkedHashMap<>();
public NewsService() throws IOException {
}
#Override
public List<News> getNews() {
Document doc = Jsoup.connect("http://www.up.krakow.pl/uniwersytet/aktualnosci").get();
Elements links = doc.select("div.page-header");
List<News> newsList = new ArrayList<>();
for (Element element : links) {
String title = element.select("a[href]").text(); // get only text
String url = "http://www.up.krakow.pl" + element.select("a[href]").attr("href"); // get only link
if (!store.containsKey(title)) {
store.put(title, url);
}
}
for (Map.Entry<String, String> entry : store.entrySet()) {
newsList.add(new News(entry.getKey(), entry.getValue()));
}
return Lists.reverse(newsList);
}
}
This is just a fix for getting always the same values. Depending of how often your service is called, this might lead to lots of requests to the backend server you are querying. In this case you should add some kind of cache which will for example only fetch new data from the back when the last one is too old. But that's a different story.
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
I'm looking for possibility to serialize transient information only in some cases:
#JsonInclude(Include.NON_NULL)
#Entity
public class User {
public static interface AdminView {}
... id, email and others ...
#Transient
private transient Details details;
#JsonIgnore // Goal: ignore all the time, except next line
#JsonView(AdminView.class) // Goal: don't ignore in AdminView
public Details getDetails() {
if (details == null) {
details = ... compute Details ...
}
return details;
}
}
public class UserDetailsAction {
private static final ObjectWriter writer = new ObjectMapper();
private static final ObjectWriter writerAdmin = writer
.writerWithView(User.AdminView.class);
public String getUserAsJson(User user) {
return writer.writeValueAsString(user);
}
public String getUserAsJsonForAdmin(User user) {
return writerAdmin.writeValueAsString(user);
}
}
If I call getUserAsJson I expected to see id, email and other fields, but not details. This works fine. But I see same for getUserAsJsonForAdmin, also without detail. If I remove #JsonIgnore annotation - I do see details in both calls.
What do I wrong and is there good way to go? Thanks!
You may find the use of the dynamic Jackson filtering slightly more elegant for your use case. Here is an example of the filtering of POJO fields based on a custom annotation sharing one object mapper instance:
public class JacksonFilter {
static private boolean shouldIncludeAllFields;
#Retention(RetentionPolicy.RUNTIME)
public static #interface Admin {}
#JsonFilter("admin-filter")
public static class User {
public final String email;
#Admin
public final String details;
public User(String email, String details) {
this.email = email;
this.details = details;
}
}
public static class AdminPropertyFilter extends SimpleBeanPropertyFilter {
#Override
protected boolean include(BeanPropertyWriter writer) {
// deprecated since 2.3
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
if (writer instanceof BeanPropertyWriter) {
return shouldIncludeAllFields || ((BeanPropertyWriter) writer).getAnnotation(Admin.class) == null;
}
return true;
}
}
public static void main(String[] args) throws JsonProcessingException {
User user = new User("email", "secret");
ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(new SimpleFilterProvider().addFilter("admin-filter", new AdminPropertyFilter()));
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
shouldIncludeAllFields = true;
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
}
}
Output:
{
"email" : "email"
}
{
"email" : "email",
"details" : "secret"
}
It's look like jackson have horrible concept on very cool feature like #JsonView. The only way I discover to solve my problem is:
#JsonInclude(Include.NON_NULL)
#Entity
public class User {
public static interface BasicView {}
public static interface AdminView {}
... id and others ...
#JsonView({BasicView.class, AdminView.class}) // And this for EVERY field
#Column
private String email;
#Transient
private transient Details details;
#JsonView(AdminView.class)
public Details getDetails() {
if (details == null) {
details = ... compute Details ...
}
return details;
}
}
public class UserDetailsAction {
private static final ObjectWriter writer = new ObjectMapper()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.writerWithView(User.BasicView.class);
private static final ObjectWriter writerAdmin = new ObjectMapper()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.writerWithView(User.AdminView.class);
public String getUserAsJson(User user) {
return writer.writeValueAsString(user);
}
public String getUserAsJsonForAdmin(User user) {
return writerAdmin.writeValueAsString(user);
}
}
Maybe it's help some one. But I hope to find better solution and because doesn't accept my own answer.
EDIT: because interface can extends (multiple) interfaces, I can use:
public static interface AdminView extends BasicView {}
and just
#JsonView(BasicView.class)
instead of
#JsonView({BasicView.class, AdminView.class})