I'm trying to figure out how to pass a collection of optional parameters to a SpringBoot controller, along with a multipart file, but can't figure out how.
Without the multipart file, this works fine
public ResponseEntity<ViewModel> performOperationOnFilePath(#Valid #RequestBody final FileOperationFileContract requestBody)
where FileOperationFileContract is a POJO that looks like this
public class FileOperationFileContract {
#NotBlank(message = "The filepath is required")
private String filePath;
private Map<String, String> options = new HashMap<>();
#JsonAnyGetter
public Map<String, String> getOptions() {
return options;
}
#JsonAnySetter
public void setOptions(final String name, final String value) {
options.put(name, value);
}
}
Here's what I tried with the multipart file
public ResponseEntity<ViewModel> performOperationOnFile(#RequestParam(name="myfile") final MultipartFile file, #RequestPart(name="options", required=false) final FileOperationMultipartFileContract requestBody)
where FileOperationMultipartFileContract is a POJO that looks like this
public class FileOperationMultipartFileContract {
private Map<String, String> options = new HashMap<>();
#JsonAnyGetter
public Map<String, String> getOptions() {
return options;
}
#JsonAnySetter
public void setOptions(final String name, final String value) {
options.put(name, value);
}
}
This seems to work if I specify the Json payload in Postman like this (note that I must specify the content-type as application/json, or I get the message "'application/octet-stream' not supported"
But if there is no json part sent, I get a 403. I want the option collection to just be empty or null. In other words, I want it to default to {}
I've tried RequestPart and RequestParam, but there's no difference. I've also tried it with and without the #Valid annotation
How can I do this?
Update: I think that I figured out that my problem does not have as much to do with Spring as it does with what I'm trying to do with the #JsonAnyGetter and #JsonAnySetter annotations. If I change FileOperationFileContract to have regular fields, it works. But my guess is that the way I have it doesn't work very well with null. Although if the value is null, there should be no calls to setOptions. Any ideas?
Related
In chat application, there are many rooms(Map type), which consist of Strings, boolean, and List<WebSocketSession>.
I think the problem is List<WebSocketSession> can't be written to JSON.
#RequestMapping(value = "/api/v1/lobby/roomList", method = RequestMethod.GET)
public ResponseEntity<Object> getRooms(
HttpServletRequest request,
HttpServletResponse response) {
logger.debug("RoomCtrl - getRooms");
Map<Integer, Room> rooms = roomService.getRooms();
Map<String, Object> returnMap = new HashMap<>();
returnMap.put("rooms", rooms);
return new ResponseEntity<>(
returnMap,
HttpStatus.OK);
}
This is my method to get rooms from roomService. What I have to do to receive that response correctly?
For giving more information to you, I post Room Class.
public class Room {
private String host, title;
private List<WebSocketSession> members = new ArrayList<>();
private boolean status;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public List<WebSocketSession> getMembers() {
return members;
}
public void setMembers(List<WebSocketSession> members) {
this.members = members;
}
public boolean isStatus() {
return status;
}
public void setStatus(boolean status) {
this.status = status;
}
}
A WebSocketSession is an abstraction to send messages over a WebSocket.
In the backend you can maintain WebSocketSession instances (while generally you maintain some specific values of WebSocketSession such as id) to perform some matching (who does the request and so for...) but you will never expose and send them in a JSON object to the clients.
Why send such a payload to the clients ? Why should they know the network details of the other users (IP, sessionID and so for) ? It will just create an overhead and decrease the security level of your application.
So WebSocketSession doesn't implement Serializable and so is defacto not serializable by Jackson (that requires that as most of JSON processing API in Java).
To resolve your issue :
Since the users need to know the name of the other chat room users you should
create a mapping between WebSocketSession.id and their unique pseudo/username with a Map<String, String> for example.
And now expose List<String> users in the JSON object returned.
Yes, you are right, WebSocketSession not json serializable, and anyway you better dont send this info to clientside.
You can use json ignore annotation here
#JsonIgnore
private List<WebSocketSession> members
so Jackson will ignore this field, when trying to serialize Room object
I am working with some json objects that I call verbose:
{
"user": {
"name": "username",
"email": "blah#blah.com",
"time_zone": "America/New_York"
}
}
But I'd prefer to just deal with them in terms of java POJOs like:
class UserDetails {
String name;
String email;
String timeZone;
...
}
Note that I have no control over the POJO as it is generated code.
My two requirements for (de)serialization is that
the timeZone field maps to time_zone in JSON
the outer user is ignored
So I have some customer (de)serializers:
class UserDeserializer implements JsonDeserializer<UserDetails> {
#Override
public UserDetails deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
throws JsonParseException {
JsonElement content = je.getAsJsonObject().get("user");
UserDetails userDetails = new GsonBuilder()
.setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create()
.fromJson(content, UserDetails.class);
return userDetails;
}
}
class UserSerializer implements JsonSerializer<UserDetails> {
#Override
public JsonElement serialize(UserDetails userDetails, Type typeOfSrc,
JsonSerializationContext context) {
JsonObject obj = new JsonObject();
JsonElement je = new GsonBuilder()
.setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create().toJsonTree(userDetails);
obj.add("user", je);
return obj;
}
}
I feel like creating new Gson objects in the (de)serializer logic is not ideal/efficient just to add and remove the outermost user key.
EDIT: Actually .setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) does work fine on deserialization.
I don't really think it's a good idea in general, and you should probably better have a single Wrapper<T> for all "top-most" purposes (if you don't want your inner objects to be considered "verbose").
But you're right when you say
I feel like creating new Gson objects in the (de)serializer logic is not ideal/efficient just to add and remove the outermost user key.
So:
Creating a Gson is a relatively expensive operation.
This just creates unnecessary objects and hits the heap.
Gson may be configured in a special way and you might want to share the same Gson configuration everywhere.
JsonSerializer and JsonDeserializer operate on JSON trees (JsonElement and its subclasses), therefore it creates an intermediate in-memory tree representations before/after serialization/deserialization.
You might consider a faster solution, that's free of those items.
final class VerboseTypeAdapterFactory
implements TypeAdapterFactory {
private final Map<Class<?>, String> mappings;
private VerboseTypeAdapterFactory(final Map<Class<?>, String> mappings) {
this.mappings = mappings;
}
static TypeAdapterFactory get(final Map<Class<?>, String> mappings) {
// Create a defensive copy to make sure the map is not modified from outside
final Map<Class<?>, String> mappingsCopy = mappings
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return new VerboseTypeAdapterFactory(mappingsCopy);
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final Class<? super T> rawType = typeToken.getRawType();
// Not something we can handle?
if ( !mappings.containsKey(rawType) ) {
// Then let Gson do its job elsewhere
return null;
}
// Getting a property name we want to use for a particular class
final String propertyName = mappings.get(rawType);
// And getting the original type adapter for this class (effectively ReflectiveTypeAdapterFactory.Adapter)
final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
return VerboseTypeAdapter.get(propertyName, delegateTypeAdapter);
}
private static final class VerboseTypeAdapter<T>
extends TypeAdapter<T> {
private final String propertyName;
private final TypeAdapter<T> delegateTypeAdapter;
private VerboseTypeAdapter(final String propertyName, final TypeAdapter<T> delegateTypeAdapter) {
this.propertyName = propertyName;
this.delegateTypeAdapter = delegateTypeAdapter;
}
private static <T> TypeAdapter<T> get(final String propertyName, final TypeAdapter<T> delegateTypeAdapter) {
return new VerboseTypeAdapter<>(propertyName, delegateTypeAdapter)
// A convenient method to simplify null-handling
.nullSafe();
}
#Override
#SuppressWarnings("resource")
public void write(final JsonWriter out, final T object)
throws IOException {
// Open the object with `{`
out.beginObject();
// Prepend the object with its reserved name
out.name(propertyName);
// Write the object
delegateTypeAdapter.write(out, object);
// And close the object with `}`
out.endObject();
}
#Override
public T read(final JsonReader in)
throws IOException {
// Assume the very first token is `{`
in.beginObject();
// Peeking what's the actual property name
final String actualPropertyName = in.nextName();
// And if it's not we expect, throw a JSON parse exception
if ( !actualPropertyName.equals(propertyName) ) {
throw new JsonParseException("Expected " + propertyName + " but was " + actualPropertyName);
}
// Otherwise read the value led by the property name
final T object = delegateTypeAdapter.read(in);
// And make sure there are no more properties
if ( in.hasNext() ) {
throw new JsonParseException(propertyName + " is expected to be the only top-most property");
}
// Assume the very last token is `}` (this works for the check above, but we made it more semantical)
in.endObject();
return object;
}
}
}
So, for example, the following code
private static final Gson gson = new GsonBuilder()
.setFieldNamingStrategy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapterFactory(VerboseTypeAdapterFactory.get(ImmutableMap.of(UserDetails.class, "user")))
.create();
...
final UserDetails userDetails = gson.fromJson(jsonReader, UserDetails.class);
System.out.println(userDetails.name);
System.out.println(userDetails.email);
System.out.println(userDetails.timeZone);
final String json = gson.toJson(userDetails);
System.out.println(json);
produces
username
blah#blah.com
America/New_York
{"user":{"name":"username","email":"blah#blah.com","time_zone":"America/New_York"}}
As the conclusion:
No more excessive Gson instantiation.
Original Gson instance configuration inherited (i.e. FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES set once).
No intermediate JsonElement instances.
I am working on an embedded jersey instance which will run a JAXB RESTful service. I have configured Jackson with two steps:
Adding this to my POM
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.23.2</version>
</dependency>
Registering it in my application
public HandheldApplication() {
scripts.add(HandheldServer.class);
scripts.add(BasicScript.class);
// Add JacksonFeature.
scripts.add(JacksonFeature.class);
scripts.add(LoggingFilter.class);
}
I have a complex object being passed back and forth as shown below:
package com.ziath.handheldserver.valueobjects;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.*;
#SuppressWarnings("restriction")
#XmlRootElement
public class Widget {
private String key;
private String name;
private List<String> options = new ArrayList<String>();
private String value;
private String type;
public Widget(){
super();
}
public Widget(String key, String name, List<String> options, String value,
String type) {
super();
this.key = key;
this.name = name;
this.options = options;
this.value = value;
this.type = type;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<String> getOptions() {
return options;
}
public void setOptions(List<String> options) {
this.options = options;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
When I execute this in a GET method as shown below:
#Override
#GET
#Path("getKeys")
#Produces(MediaType.APPLICATION_JSON)
public List<Widget> getKeys(#QueryParam(value = "page") int page)
This works fine and I get JSON back; however when I execute it is a PUT as shown below:
#Override
#PUT
#Path("validateKeys")
#Produces({MediaType.APPLICATION_JSON})
#Consumes(MediaType.APPLICATION_JSON)
public boolean validateKeys(#QueryParam(value = "page")int page, #QueryParam(value = "widgets")List<Widget> widgets)
When I execute a PUT to access this method I get a stack trace as follows:
Caused by: org.glassfish.jersey.internal.inject.ExtractorException: Error un-marshalling JAXB object of type: class com.ziath.handheldserver.valueobjects.Widget.
at org.glassfish.jersey.jaxb.internal.JaxbStringReaderProvider$RootElementProvider$1.fromString(JaxbStringReaderProvider.java:195)
at org.glassfish.jersey.server.internal.inject.AbstractParamValueExtractor.convert(AbstractParamValueExtractor.java:139)
at org.glassfish.jersey.server.internal.inject.AbstractParamValueExtractor.fromString(AbstractParamValueExtractor.java:130)
at org.glassfish.jersey.server.internal.inject.CollectionExtractor.extract(CollectionExtractor.java:88)
at org.glassfish.jersey.server.internal.inject.CollectionExtractor$ListValueOf.extract(CollectionExtractor.java:107)
at org.glassfish.jersey.server.internal.inject.QueryParamValueFactoryProvider$QueryParamValueFactory.provide(QueryParamValueFactoryProvider.java:89)
... 38 more
Caused by: javax.xml.bind.UnmarshalException
- with linked exception:
[org.xml.sax.SAXParseException; lineNumber: 1; columnNumber: 1; Content is not allowed in prolog.]
at javax.xml.bind.helpers.AbstractUnmarshallerImpl.createUnmarshalException(AbstractUnmarshallerImpl.java:335)
at com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallerImpl.createUnmarshalException(UnmarshallerImpl.java:563)
at com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:249)
at com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal(UnmarshallerImpl.java:214)
at javax.xml.bind.helpers.AbstractUnmarshallerImpl.unmarshal(AbstractUnmarshallerImpl.java:140)
at javax.xml.bind.helpers.AbstractUnmarshallerImpl.unmarshal(AbstractUnmarshallerImpl.java:123)
at org.glassfish.jersey.jaxb.internal.JaxbStringReaderProvider$RootElementProvider$1.fromString(JaxbStringReaderProvider.java:190)
... 43 more
So it seems to me that Jackson is correctly marshalling my POJO into JSON but trying to unmarshall it as XML. Note that I switched to Jackson away from MOXy because I needed to be able to handle collections coming back and forth and apparently MOXy cannot do that.
Is there a setting I've missed to tell Jackson/Jersey to go both ways for JSON?
Try removing #QueryParam(value = "widgets") because you should pass it as entity body - not query param.
#PUT
#Path("validateKeys")
#Produces({MediaType.APPLICATION_JSON})
#Consumes(MediaType.APPLICATION_JSON)
public boolean validateKeys(#QueryParam(value = "page")int page, List<Widget> widgets)
Also you can make wrapper class:
#XmlRootElement
public class Widgets {
private List<Widget> widgets;
// other fields, setters and getters
}
And then:
#PUT
#Path("validateKeys")
#Produces({MediaType.APPLICATION_JSON})
#Consumes(MediaType.APPLICATION_JSON)
public boolean validateKeys(#QueryParam(value = "page")int page, Widgets widgets)
I would suggest to read some discussions about REST design because you're using verbs in your paths:
Is this a bad REST URL?
Understanding REST: Verbs, error codes, and authentication
I was switching between QueryParam and FormParam to try and get one of them to work. If I use FormParam I also need to change the consumes to APPLICATION_FORM_URLENCODED.
The actual issue was that the default unmarshalling with Jackson was using XML because it was tagged as an XML resource - take that out! I finally managed to work out how to unmarshall from JSON by using a static fromString method. Then to handle the list; I cannot use a wrapper class because this needs to be highly cross language and exposing a wrapper with a list would have complicated the implementation from Python, C#, etc. The way to get it to accept a list with a wrapper is to post the name of the param (in this case widgets) multiple time. Then each JSON passed in will be called against the fromString method.
I have two controllers in my micro service both are POST and accepts Request body as JSON, one is working fine and another one's JSON input from some othet team and it is with root class name , so I need to write custom object mapper for this later controller, could you please guys help,
please find the codes below,
#RestController
#Slf4j
public class Controller2 {
#RequestMapping(value = "/some/update", method = RequestMethod.POST)
public String updateEmd(#RequestBody final UpdateEMDRequest updateEMDRequest) throws JsonProcessingException {
updateEMDRequest.getBookingReference()); // null now
return "success";
}
}
and the sample json is as follows,
{
"UpdateEMDRequest":{
"TransactionStatus":"SUCCESS",
"UniqueTransactionReference":"046060420",
"PreAuthReference":"040520420",
"BookingReference":"8PJ",
"CarrierCode":"AS",
"TransactionMode":"Batch",
"CallBackUrl":"www.test.com/op/update",
"Offers":[
{
"Offer":{
"traveler":{
"firstName":"AHONY",
"surname":"DNEN",
"EMD":[
"081820470"
]
}
}
}
]
}
}
UpdateEMDRequest,java
#JsonInclude(Include.NON_NULL)
public class UpdateEMDRequest {
#JsonProperty("UniqueTransactionReference")
private String uniqueTransactionReference;
#JsonProperty("TransactionStatus")
private String transactionStatus;
#JsonProperty("PreAuthReference")
private String preAuthReference;
#JsonProperty("BookingReference")
private String bookingReference;
#JsonProperty("CarrierCode")
private String carrierCode;
#JsonProperty("TransactionMode")
private String transactionMode;
#JsonProperty("CallBackUrl")
private String callBackUrl;
#JsonProperty("Offers")
private List<Offers> offers;
}
So this json is not parsed properly and updateEMDRequest's properties are null always.
I am trying to map an array from a backend api call. How can I map this data knowing that :
the following classes will be used to hold the json array data :
#Data
private static class UserListBean {
private List<UserBean> userList;
}
#Data
private static class UserBean {
private String id;
private String userName;
private String password;
}
the json will have the following format (the following example just have one item in it) :
[
{
"id":1,
"userName":"bob",
"password":"403437d5c3f70b1329f37a9ecce02adbbf3e986"
}
]
I am using Jackson and I have tried the following so far but it keeps sending me back an exception
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
final ObjectReader reader = mapper.reader(UserListBean.class);
GeoHubAccountListBean accounts = null;
try {
accounts = reader.readValue(jsonString);
} catch (final IOException ex) {
log.error("Cannot convert JSON into a list of users", ex);
}
Here the final ObjectReader reader = mapper.reader(UserListBean.class); throws an exception
Can not deserialize instance of com.xxx.XXX$UserListBean out of START_ARRAY token
Any idea ?
thanks
Well, if you are trying to deserialize json to an object of type UserListBean, then you need to deserialize a JSONObject (Java Objects tend to map to JSONObjects).
Therefore, your outer most json construct should be an object. Your outer most json construct is a JSONArray.
Your UserListBean has a single field, which is a List<UserBean>. So your top level json construct (which is a JSONObject) should contain a single field with the name 'userList' with a value that is a JSONArray (Java Collections tend to map to JSONArrays).
I think this is the actual json you are looking for:
{
"userList":[
{
"id":1,
"userName":"bob",
"password":"403437d5c3f70b1329f37a9ecce02adbbf3e986"
}
]
}
If you have no control over the json coming in, then you probably want to ditch the parent object UserListBean and deal directly with the List<UserBean>, as that would work with the json you have provided.
Try something like the following:
public static <T> T mapFromJson(String json, Class<T> clazz) throws JsonParseException, JsonMappingException, IOException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return objectMapper.readValue(json, clazz);
}
public static void main(String[] args) throws IOException {
HeaVO HeaVO=new HearVO();
HeaVO.setId("Id");
List<HeaVO> listVO=new ArrayList<HeaVO>();
listVO.add(HeaVO);
List<HeaVO> listHeaVORes=Arrays.asList(mapFromJson(mapToJson(listHeaVO), HeaVO[].class));
System.out.println(((HeaVO)listVORes.get(0)).getId());
}