I am trying to parsing a Soap response string to a JAVA object to get those parameters.
Here's the response string:
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns0:submitCdus1Response xmlns:ns0="http://example.com/">
<MessageId>D421425215</MessageId>
<NoOfDay>14</NoOfDay>
<Status>Y</Status>
<LastControlPoint>SBC</LastControlPoint>
<LastEntryDate>20210415</LastEntryDate>
<ReplyDateTime>20210427114126848</ReplyDateTime>
<TypeOfTravel>A</TypeOfTravel>
</ns0:submitCdus1Response>
</S:Body>
</S:Envelope>
My Object class:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(name="submitCdus1Response", namespace="http://example.com/" )
public class TravelHistoryResponseDTO implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
#XmlAttribute(name = "MessageId")
private String MessageId;
#XmlAttribute(name = "NoOfDay")
private Integer NoOfDay;
#XmlAttribute(name = "Status")
private String Status; //Y , N ,
#XmlAttribute(name = "LastControlPoint")
private String LastControlPoint;
#XmlAttribute(name = "TypeOfTravel")
private String TypeOfTravel;// A
#XmlAttribute(name = "LastEntryDate")
private Date LastEntryDate;
#XmlAttribute(name = "ReplyDateTime")
private Date ReplyDateTime;
public String getMessageId() {
return MessageId;
}
public void setMessageId(String messageId) {
MessageId = messageId;
}
public Integer getNoOfDay() {
return NoOfDay;
}
public void setNoOfDay(Integer noOfDay) {
NoOfDay = noOfDay;
}
public String getStatus() {
return Status;
}
public void setStatus(String status) {
Status = status;
}
public String getLastControlPoint() {
return LastControlPoint;
}
public void setLastControlPoint(String lastControlPoint) {
LastControlPoint = lastControlPoint;
}
public String getTypeOfTravel() {
return TypeOfTravel;
}
public void setTypeOfTravel(String typeOfTravel) {
TypeOfTravel = typeOfTravel;
}
public Date getLastEntryDate() {
return LastEntryDate;
}
public void setLastEntryDate(Date lastEntryDate) {
LastEntryDate = lastEntryDate;
}
public Date getReplyDateTime() {
return ReplyDateTime;
}
public void setReplyDateTime(Date replyDateTime) {
ReplyDateTime = replyDateTime;
}
#Override
public String toString() {
return "TravelHistoryResponseDTO [MessageId=" + MessageId + ", NoOfDay=" + NoOfDay + ", Status=" + Status
+ ", LastControlPoint=" + LastControlPoint + ", TypeOfTravel=" + TypeOfTravel + ", LastEntryDate="
+ LastEntryDate + ", ReplyDateTime=" + ReplyDateTime + "]";
}
}
My code:
SOAPMessage message = MessageFactory.newInstance().createMessage(null,
new ByteArrayInputStream(responseString.getBytes()));
Unmarshaller unmarshaller = JAXBContext.newInstance(TravelHistoryResponseDTO.class).createUnmarshaller();
TravelHistoryResponseDTO dto = (TravelHistoryResponseDTO)unmarshaller.unmarshal(message.getSOAPBody().extractContentAsDocument());
But I am getting
javax.xml.bind.UnmarshalException: unexpected element (uri:http://example.com/", local:"submitCdus1Response"). Expected elements are <{}submitCdus1Response>
Anyway to solve this and able to map the parameters to the java object?
There is an error in the JAXB object mapping : you are using #XmlAttribute for the various fields of your object, but the XML data you are trying to read are not XML attributes (<ns0:submitCdus1Response MessageId="xxx">) but XML elements (<MessageId>xxx</MessageId>).
Therefore all you have to is switch from #XmlAttribute to #XmlElement to match the mapping object to your actual XML.
As a note : if this comes really out of a SOAP call, chances are that you have a XSD Schema somewhere that defines the XML structure that is used here. JAXB comes with a code generation tool that will create the JAXB Objects (e.g. TravelHistoryResponseDTO) for you, deriving the data from the XSD. The tool is called xjc (you can google / search for it), it comes with the JDK and or can be found in maven plugins, (and more). This is the kind of mistake the tool would have prevented.
Related
When I try to send it, it show me the "error": "Bad Request",
"trace": "...HttpMessageNotReadableException: JSON parse error: Cannot deserialize Map key of type java.time.LocalDate from String "quoteDate": Failed to deserialize java.time.LocalDate"
The JSON I am sending via postman:
{
"stockId":"test3",
"quotes":[
{
"quoteDate":"2003-05-14",
"quoteValue":"35.9"
},
{
"quoteDate":"2016-03-28",
"quoteValue":"55.0"
}
]
}
The controller:
#PostMapping("/stock/save")
public void saveQuotes(#RequestBody StockDTO stockDTO) {
System.out.println(stockDTO.toString());
}
The DTO
public class StockDTO {
String id;
String stockId;
Map<LocalDate, Double> quotes;
}
For quotes, the data type that should come as per the json string is a List. but was given a Map in the DTO.
The DTO should be ::
import com.google.gson.Gson;
import java.util.List;
public class GsonMappingExample {
public static void main(String[] args) {
String jsonString = "{\"stockId\":\"test3\",\"quotes\":[{\"quoteDate\":\"2003-05-14\",\"quoteValue\":\"35.9\"},{\"quoteDate\":\"2016-03-28\",\"quoteValue\":\"55.0\"}]}";
Gson gson = new Gson();
StockDTO stockDTO = gson.fromJson(jsonString, StockDTO.class);
System.out.println(stockDTO);
}
}
class StockDTO {
private final String stockId;
private final List<Quote> quotes;
public StockDTO(String stockId, List<Quote> quotes) {
this.stockId = stockId;
this.quotes = quotes;
}
#Override
public String toString() {
return "StockDTO{" +
"stockId='" + stockId + '\'' +
", quoteList=" + quotes +
'}';
}
}
class Quote {
private final String quoteDate;
private final Double quoteValue;
public Quote(String quoteDate, Double quoteValue) {
this.quoteDate = quoteDate;
this.quoteValue = quoteValue;
}
#Override
public String toString() {
return "Quote{" +
"quoteDate=" + quoteDate +
", quoteValue=" + quoteValue +
'}';
}
}
PS: Here I'm using Gson library to parse the json string, spring-boot automatically does that (using Jackson library I think!)
I have the following Json
{
"coreId" : "1",
"name" : "name",
"additionalValueList" : [
{
"columnName" : "allow_duplicate",
"rowId" : "10",
"value" : "1"
},
{
"columnName" : "include_in_display",
"rowId" : "11",
"value" : "0"
},
...e.t.c
]
},
...e.t.c
and Java class
class DTO {
#JsonProperty("coreId")
private Integer id;
private String name;
private Boolean allowDuplicate;
private Boolean includeInDisplay;
}
How I can easily map values from 'additionalValueList' to corresponding java fields.For example Json value from field 'columnName' - 'allow_duplicate' = DTO.allowDuplicate.
Actually I know how to do it with custom deserializers with #JsonDeserialize annotation and smth like this.Bu I have 40+ DTO and it is not a good idea to create own deserializer for each filed. I am looking for solution to have for example 1 deserializer(since values structure in 'additionalValueList' are the same for all entities) and to pass parameter(field name that I want to map to that field) to custom deserializer that will find in 'additionalValueList' entity with 'column Name' = parameter(that I passed from annotation) and return 'value'.
Example
class DTO {
#JsonProperty("coreId")
private Integer id;
private String name;
#JsonDeserialize(using = MyCustDeser.class,param = allow_duplicate)
private Boolean allowDuplicate;
#JsonDeserialize(using = MyCustDeser.class,param = include_in_display)
private Boolean includeInDisplay;
}
It will be a good solution but maybe not easy to achieve.However I will be very grateful for all your advices.Thank you.
Create a Converter class, then specify it on the DTO class.
The following code uses public fields for the simplicity of the example.
/**
* Intermediate object used for deserializing FooDto from JSON.
*/
public final class FooJson {
/**
* Converter used when deserializing FooDto from JSON.
*/
public static final class ToDtoConverter extends StdConverter<FooJson, FooDto> {
#Override
public FooDto convert(FooJson json) {
FooDto dto = new FooDto();
dto.name = json.name;
dto.id = json.coreId;
dto.allowDuplicate = lookupBoolean(json, "allow_duplicate");
dto.includeInDisplay = lookupBoolean(json, "include_in_display");
return dto;
}
private static Boolean lookupBoolean(FooJson json, String columnName) {
String value = lookup(json, columnName);
return (value == null ? null : (Boolean) ! value.equals("0"));
}
private static String lookup(FooJson json, String columnName) {
if (json.additionalValueList != null)
for (FooJson.Additional additional : json.additionalValueList)
if (columnName.equals(additional.columnName))
return additional.value;
return null;
}
}
public static final class Additional {
public String columnName;
public String rowId;
public String value;
}
public Integer coreId;
public String name;
public List<Additional> additionalValueList;
}
You now simply annotate the DTO to use it:
#JsonDeserialize(converter = FooJson.ToDtoConverter.class)
public final class FooDto {
public Integer id;
public String name;
public Boolean allowDuplicate;
public Boolean includeInDisplay;
#Override
public String toString() {
return "FooDto[id=" + this.id +
", name=" + this.name +
", allowDuplicate=" + this.allowDuplicate +
", includeInDisplay=" + this.includeInDisplay + "]";
}
}
Test
ObjectMapper mapper = new ObjectMapper();
FooDto foo = mapper.readValue(new File("test.json"), FooDto.class);
System.out.println(foo);
Output
FooDto[id=1, name=name, allowDuplicate=true, includeInDisplay=false]
I want the Country class to store the "ISO_3166-1_Alpha-2_Code" code. The code currently gets back the "ISO_3166-1_Numeric-3_Code" code. Can't figure out how to tweak the Country class to get the specific attribute I want.
XML:
<?xml version='1.0' encoding='UTF-8'?>
<wd:Message_Event_Configuration">
<wd:Message_Event_Configuration_Data>
<wd:Country_Reference wd:Descriptor="Saint Martin">
<wd:ID wd:type="WID">66b7082a21e510000961bb6d82b5002a</wd:ID>
<wd:ID wd:type="ISO_3166-1_Alpha-2_Code">MF</wd:ID>
<wd:ID wd:type="ISO_3166-1_Alpha-3_Code">MAF</wd:ID>
<wd:ID wd:type="ISO_3166-1_Numeric-3_Code">663</wd:ID>
</wd:Country_Reference>
<wd:Country_Reference wd:Descriptor="Saint Barthelemy">
<wd:ID wd:type="WID">881527f6cec910000ba81e8dccf61127</wd:ID>
<wd:ID wd:type="ISO_3166-1_Alpha-2_Code">BL</wd:ID>
<wd:ID wd:type="ISO_3166-1_Alpha-3_Code">BLM</wd:ID>
<wd:ID wd:type="ISO_3166-1_Numeric-3_Code">652</wd:ID>
</wd:Country_Reference>
</wd:Message_Event_Configuration_Data>
</wd:Message_Event_Configuration>
Country List:
#XmlRootElement(name = "Message_Event_Configuration")
#XmlAccessorType(XmlAccessType.FIELD)
public class Countries {
#XmlElementWrapper(name = "Message_Event_Configuration_Data")
#XmlElement(name = "Country_Reference")
private List<Country> countries = new ArrayList<Country>();
public List<Country> getCountries() {
return countries;
}
public void setCountries(List<Country> countries) {
this.countries = countries;
}
}
Country:
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(name = "Country_Reference")
public class Country {
#XmlElement(name = "ID")
private String isoCode;
public Country() {
}
public Country(String isoCode) {
this.isoCode = isoCode;
}
#XmlAttribute(name = "ISO_3166-1_Alpha-2_Code")
public String getISOCode() {
return isoCode;
}
public void setISOCode(String isoCode) {
this.isoCode = isoCode;
}
}
The <Country_Reference> XML element contains the ISO codes in a rather
sophisticated way within several <wd:ID> XML elements.
It is therefore much too simple to model them as a Java String property.
Instead, you need to model the Java-structure with more similarity to the XML-structure.
The sequence of XML elements <wd:ID> can be modeled by a property List<ID> idList
which needs to be annotated by#XmlElement(name="ID") .
The XML attribute wd:Descriptor="...." can be modeled by a property String descriptor
which needs to be annotated by #XmlAttribute(name="Descriptor").
For your convenience you can add an all-arguments-constructor and some methods for getting
the WID and ISO codes from the List<ID>.
#XmlAccessorType(XmlAccessType.FIELD)
public class Country {
#XmlAttribute(name = "Descriptor")
private String descriptor;
#XmlElement(name = "ID")
private List<ID> idList;
public Country() {
}
public Country(String descriptor, String wid, String isoAlpha2Code, String isoAlpha3Code, String isoNumeric3Code) {
this.descriptor = descriptor;
idList = new ArrayList<>();
idList.add(new ID("WID", wid));
idList.add(new ID("ISO_3166-1_Alpha-2_Code", isoAlpha2Code));
idList.add(new ID("ISO_3166-1_Alpha-3_Code", isoAlpha3Code));
idList.add(new ID("ISO_3166-1_Numeric-3_Code", isoNumeric3Code));
}
public String getWid() {
return getIdByType("WID");
}
public String getIsoAlpha2Code() {
return getIdByType("ISO_3166-1_Alpha-2_Code");
}
public String getIsoAlpha3Code() {
return getIdByType("ISO_3166-1_Alpha-3_Code");
}
public String getIsoNumeric3Code() {
return getIdByType("ISO_3166-1_Numeric-3_Code");
}
private String getIdByType(String idType) {
for (ID id : idList) {
if (id.getType().equals(idType))
return id.getValue();
}
return null;
}
}
The XML elements <wd:ID> are quite complex. Therefore we need a separate POJO class for modeling them.
Let's call the class ID.
The XML text between <wd:ID ..> and </wd:ID> is modeled by the property String value
which needs to be annotated by #XmlValue.
The XML attribute wd:type="..." is modeled by the property String type
which needs to be annotated by #XmlAttribute.
For convenient use by the class Country above, an all-arguments-constructor is added.
#XmlAccessorType(XmlAccessType.FIELD)
public class ID {
#XmlAttribute
private String type;
#XmlValue
private String value;
public ID() {
}
public ID(String type, String value) {
this.type = type;
this.value = value;
}
// public getters and setters (omitted here fro brevity)
}
The screenshot below (taken from within the debugger) visualizes the Java structure
and confirms that the unmarshalling of your XML example works correctly:
JSON Request:
{
"notificationType" : "ISSUER_OTP1ee2asasa",
"content" : "hi fff this is fff template content for SBI email good and mobile dfdfdfd and remaining balance is 333 and your name is hahaha.",
"medium" : "EMAIL",
"asa":"ddddd",
"":""
}
POJO:
package com.innoviti.notification.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
#Document(collection = "NotificationTemplate")
#JsonIgnoreProperties(ignoreUnknown=false)
public class NotificationTemplate {
#JsonCreator
public NotificationTemplate(#JsonProperty(value="notificationType",required=true)String notificationType,
#JsonProperty(value="content",required=true)String content, #JsonProperty(value="medium",required=true)String medium) {
super();
this.notificationType = notificationType;
this.content = content;
this.medium = medium;
}
#Override
public String toString() {
return "NotificationTemplate [id=" + id + ", templateId=" + templateId + ", notificationType="
+ notificationType + ", content=" + content + ", medium=" + medium + "]";
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
#Id
private String id;
private String templateId;
public String getTemplateId() {
return templateId;
}
public void setTemplateId(String templateId) {
this.templateId = templateId;
}
private String notificationType;
private String content;
private String medium;
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getNotificationType() {
return notificationType;
}
public void setNotificationType(String notificationType) {
this.notificationType = notificationType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Controller where payload is posted.
#PostMapping(value = "/config", consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<NotificationTemplate> configureTemplate(
#Valid #RequestBody NotificationTemplate notificationTemplate) {
NotificationTemplate notificationTemplatePersisted = null;
logger.info(
"Printing payload of template on server side" + ">>>" + notificationTemplate.toString());
try {
validatePayLoad(notificationTemplate);
notificationTemplatePersisted =
notificationTemplateService.createNotificationTemplate(notificationTemplate);
} catch (Exception de) {
logger.info(String.format("Error in saving template", de.getMessage()));
throw new RequestNotCompletedException(de.getLocalizedMessage());
}
return new ResponseEntity<NotificationTemplate>(notificationTemplatePersisted,
HttpStatus.CREATED);
}
Question:How do I validate that an uknown property has been sent as part of payload.In Existing implementation,#RequestBody maps the json without any issue.I want to throw error or validate payload if incoming json is not confirming exactly to POJO.For e.g in payload example i gave,I want to be able to throw error saying that asa is not recognized property
The Jackson property that controls this behaviour is FAIL_ON_UNKNOWN_PROPERTIES. This needs to be true in your case, to get the behaviour you describe.
It seems that since spring boot 1.2 this is set to false by default.
To set it to true add this line to your application.properties file:
spring.jackson.deserialization.fail-on-unknown-properties=true
And then you will get a JsonMappingException when there are extraneous properties in a JSON payload
One can add this class int their project and it would throw an exception if json is mismatched to the pojo class properties.
#Configuration
public class Config implements InitializingBean {
#Autowired
private RequestMappingHandlerAdapter converter;
#Override
public void afterPropertiesSet() throws Exception {
configureJacksonToFailOnUnknownProperties();
}
private void configureJacksonToFailOnUnknownProperties() {
MappingJackson2HttpMessageConverter httpMessageConverter = converter.getMessageConverters().stream()
.filter(mc -> mc.getClass()
.equals(MappingJackson2HttpMessageConverter.class))
.map(mc -> (MappingJackson2HttpMessageConverter) mc)
.findFirst()
.get();
httpMessageConverter.getObjectMapper().enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
}
I have an object that is a list of 'Level' objects and I'm testing transferring them with Spring Boot Rest Controller in 2 ways:
with JSON, in Rest Controller I use something like:
#RequestMapping(value = "/api/v1/layers/{layername}", method = RequestMethod.GET, produces = "application/json")
public #ResponseBody List<Level> query(#PathVariable String layername,
#RequestParam("northEastLat") Float northEastLat,
#RequestParam("northEastLng") Float northEastLng,
#RequestParam("northWestLat") Float northWestLat,
#RequestParam("northWestLng") Float northWestLng,
#RequestParam("southEastLat") Float southEastLat,
#RequestParam("southEastLng") Float southEastLng,
#RequestParam("southWestLat") Float southWestLat,
#RequestParam("southWestLng") Float southWestLng
) {
List<Level> poligons=levelService.obtainLevels(layername,southWestLng,southWestLat,northWestLng,northWestLat,northEastLng,northEastLat,southEastLng,southEastLat);
int i=1;
for (Level p : poligons) {
System.out.println("poligon" + i++ + " is:" + p.toString());
}
return poligons;
}
With Protostuff Protobuf format, I use something like:
#RequestMapping(value = "/api/v1/layers/{layername}", method = RequestMethod.GET,produces = "text/plain")
public String query(#PathVariable String layername,
#RequestParam("northEastLat") Float northEastLat,
#RequestParam("northEastLng") Float northEastLng,
#RequestParam("northWestLat") Float northWestLat,
#RequestParam("northWestLng") Float northWestLng,
#RequestParam("southEastLat") Float southEastLat,
#RequestParam("southEastLng") Float southEastLng,
#RequestParam("southWestLat") Float southWestLat,
#RequestParam("southWestLng") Float southWestLng
) {
List<Level> poligons=levelService.obtainLevels(layername,southWestLng,southWestLat,northWestLng,northWestLat,northEastLng,northEastLat,southEastLng,southEastLat);
LevelList list = new LevelList(poligons);
byte[] bytes;
int i=1;
for (Level p : poligons) {
System.out.println("poligon" + i++ + " is:" + p.toString());
}
Schema<LevelList> schema = RuntimeSchema.getSchema(LevelList.class);
LinkedBuffer buffer = LinkedBuffer.allocate();
try
{
bytes = ProtostuffIOUtil.toByteArray(list, schema, buffer);
}
finally
{
buffer.clear();
}
return new String(bytes);
}
The Level object format is :
[{"wkb_geometry":"{"type":"Polygon","coordinates":[[[24.446822,45.34997],[24.706508,45.352485]]]}","id":199,"level":"3","type":null}
The Level object is :
#Entity(name = "Level")
#Table(name="Level2G")
#SecondaryTables({
#SecondaryTable(name="Level3G"),
#SecondaryTable(name="Level4G")
})
public class Level implements Serializable {
private static final long serialVersionUID = 1L;
// #Column(name = "wkb_geometry",columnDefinition="Geometry")
//#Type(type = "org.hibernate.spatial.GeometryType")
#Column(name="wkb_geometry")
private /*Geometry */ String wkb_geometry;
#Id
#Column(name="id")
private Integer id;
#Column(name="level")
private String level;
#Transient
private String type;
public Level() {
}
public Level(String wkb_geometry, Integer id, String level) {
this.wkb_geometry = wkb_geometry;
this.id = id;
this.level = level;
this.type = "Feature";
}
public Level(String wkb_geometry, Integer id, String level, String type) {
this.wkb_geometry = wkb_geometry;
this.id = id;
this.level = level;
this.type = type;
}
public Object getWkb_geometry() {
return wkb_geometry;
}
public void setWkb_geometry(String wkb_geometry) {
this.wkb_geometry = wkb_geometry;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
#Override
public String toString() {
return "Level{" +
"wkb_geometry=" + wkb_geometry +
", id=" + id +
", level='" + level + '\'' +
", type='" + type + '\'' +
'}';
}
}
The LevelList object is just a List of Level objects
The problem is that with Protostuff I get a bigger payload (26 kb) comparing to JSON (3.7kb). Why?
Also for second option I also tried setting "application/octet-stream" to return bytes directly but still the same result. Also I compared speed for both JSON and protobuf; protobuf has the better performance even with a bigger payload. Any idea why?
Protostuff and Protobuf are not the same thing. Protostuff is a wrapper library that can use many different serialization formats. It also supports runtime schema generation, which you appear to be using. That runtime schema requires sending extra metadata along with the message to tell the receiver about the message's schema. I would guess that the large message you're seeing is mostly from this runtime schema data.
With standard Protobuf, the schema is not sent with the message because it is assumed that the sender and recipient already agree on a schema provided by a .proto file compiled into both programs. If you use Protobuf with a standard .proto file, you'll find that the messages it produces are much smaller than JSON.
You have at least one problem in your test.
This transformation from byte array to String is not valid:
bytes = ProtostuffIOUtil.toByteArray(list, schema, buffer);
return new String(bytes);
This constructor of String will try to parse byte array as a UTF-8 string (most probably; depends on your locale settings), but given data by definition is not valid UTF-8 string.
If you want to make better size comparison, you should write a test in a following form:
LevelList source = testData();
byte[] jsonData = generateJson(source);
byte[] protobufData = generateProtobuf(source);
System.out.println("JSON=" + jsonData.size() + " Protobuf=" + protobufData.size());
Main point here is to make your test reproducible, so that other people can repeat it.