For complicated APIs, I use the
XJC binding compiler included with JAXB to generate Java classes using the XSD schema of the XML results of a web service. This is a huge time saver.
For JSON results, there isn't a standard binding compiler to generate Java classes but since JSON and XML are almost interchangeable, I generate the Java classes the same way and fix the differences after the classes have been compiled.
One common error I get is an "unrecognized field" because of the slight differences between XML and JSON representations of data.
For example, I got this error:
com.sun.jersey.api.client.ClientHandlerException: org.codehaus.jackson.map.exc.UnrecognizedPropertyException: Unrecognized field "geonames" (Class org.learn.ws.model.jaxb.api.geonames.org.postal.countryinfo.types.GeonamesType), not marked as ignorable
when unmarshalling this JSON:
{
"geonames": [
{
"numPostalCodes": 7,
"maxPostalCode": "AD700",
"countryCode": "AD",
"minPostalCode": "AD100",
"countryName": "Andorra"
},
{
"numPostalCodes": 20260,
"maxPostalCode": "9431",
"countryCode": "AR",
"minPostalCode": "1601",
"countryName": "Argentina"
}
...lots more rows...
]
}
to this GeonamesType class:
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import java.util.ArrayList;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "geonamesType", propOrder = {
"country"
})
public class GeonamesType {
protected List<countrytype> country;
public List<countrytype> getCountry() {
if (country == null) {
country = new ArrayList<countrytype>();
}
return this.country;
}
}
and this CounryType class:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "countryType", propOrder = {
"countryCode",
"countryName",
"numPostalCodes",
"minPostalCode",
"maxPostalCode"
})
public class CountryType {
@XmlElement(required = true)
protected String countryCode;
@XmlElement(required = true)
protected String countryName;
@XmlElement(required = true)
protected String numPostalCodes;
@XmlElement(required = true)
protected String minPostalCode;
@XmlElement(required = true)
protected String maxPostalCode;
public String getCountryCode() {
return countryCode;
}
public void setCountryCode(String value) {
this.countryCode = value;
}
public String getCountryName() {
return countryName;
}
public void setCountryName(String value) {
this.countryName = value;
}
public String getNumPostalCodes() {
return numPostalCodes;
}
public void setNumPostalCodes(String value) {
this.numPostalCodes = value;
}
public String getMinPostalCode() {
return minPostalCode;
}
public void setMinPostalCode(String value) {
this.minPostalCode = value;
}
public String getMaxPostalCode() {
return maxPostalCode;
}
public void setMaxPostalCode(String value) {
this.maxPostalCode = value;
}
}
The GeonamesType class is the root-level class generated and annotated automatically with the JAXB XJC shell script. For more on XJC, see
http://www.thoughts-on-java.org/generate-your-jaxb-classes-in-second/.
The XJC shell script is used to generate Java classes from an XSD schema file. The XSD schema file can be generated automatically from sample XML with any number of online XML schema generators, for example this one:
http://www.freeformatter.com/xsd-generator.html.
The error is telling me that the root-level node in the returned JSON, "geonames", doesn't have a correspondingly-named property in the root-level class, GeonamesType. In the class, its name is "country". The class was generated with xjc from the
XML version of the result returned from the web service, which looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<geonames>
<country>
<countryCode>AD</countryCode>
<countryName>Andorra</countryName>
<numPostalCodes>7</numPostalCodes>
<minPostalCode>AD100</minPostalCode>
<maxPostalCode>AD700</maxPostalCode>
</country>
<country>
<countryCode>AR</countryCode>
<countryName>Argentina</countryName>
<numPostalCodes>20260</numPostalCodes>
<minPostalCode>1601</minPostalCode>
<maxPostalCode>9431</maxPostalCode>
</country>
...lots more rows...
</geonames>
In the XML version, geonames is a list of country objects, which is how it is represented in the Java classes. In the JSON version, geonames is an array (list) of objects,
but the objects aren't named. I could fix this by renaming "country" in the GeonamesType class to "geonames" like this:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "geonamesType", propOrder = {
"geonames"
})
public class GeonamesType {
protected List<countrytype> geonames;
public List<countrytype> getCountry() {
if (geonames == null) {
geonames = new ArrayList<countrytype>();
}
return this.geonames;
}
}
This solves the problem for JSON results, but it doesn't work anymore for the XML results.
Use Same Classes With XML And JSON
By modifying the original GeonamesType class (at the top) with a Jackson annotation that is specific to JSON, it is possible to use the same Java classes for both XML and JSON versions of the results.
Just add the @JsonElement annotation to the country property to tell Jackson that the name of the element in the JSON result will be "geonames", not "country", like this:
import org.codehaus.jackson.annotate.JsonProperty;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;
import java.util.ArrayList;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "geonamesType", propOrder = {
"country"
})
public class GeonamesType {
@JsonProperty(value = "geonames")
protected List<countrytype> country;
public List<countrytype> getCountry() {
if (country == null) {
country = new ArrayList<countrytype>();
}
return this.country;
}
}
Now the classes work for both the XML and JSON results as shown above.