Upsert Document with a generic json field - java

Using spring-data-couchbase I want to define a document with a field settings with generic JSON. To do this, I create a class
#Document
public class SampleDoc {
#Id
#NotNull
protected String id;
#Field
private JsonNode settings;
}
When I try to persist below JSON object to this document instance
{
"someField" : "someData"
}
It is persisted in the CouchBase as
"settings": {
"_children": {
"someField": {
"type": "com.fasterxml.jackson.databind.node.TextNode",
"_value": "someData"
}
},
"type": "com.fasterxml.jackson.databind.node.ObjectNode",
"_nodeFactory": {
"_cfgBigDecimalExact": false
}
}
And when I try to get the document from database through CouchbaseRepository.findById it returns error :
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments ] with root cause
How could I persist a generic JSON object to Couchbase and assure it to be stored as a simple JSON like :
{
//other fields
"settings" : {
"someField" : "someData"
}
//other fields
}
Thank you

You can create two converters like this.
Then add them as CustomConverters by overriding the customConversions() method in AbstractCouchbaseConfiguration. The only change from the original is to add the two converters.
I opened an issue at https://github.com/spring-projects/spring-data-couchbase/issues/1650
#WritingConverter
public enum JsonNodeToMap implements Converter<JsonNode, CouchbaseDocument> {
INSTANCE;
#Override
public CouchbaseDocument convert(JsonNode source) {
if( source == null ){
return null;
}
return new CouchbaseDocument().setContent(JsonObject.fromJson(source.toString()));
}
}
#ReadingConverter
public enum MapToJsonNode implements Converter<CouchbaseDocument, JsonNode> {
INSTANCE;
static ObjectMapper mapper= new ObjectMapper();
#Override
public JsonNode convert(CouchbaseDocument source) {
if( source == null ){
return null;
}
return mapper.valueToTree(source.export());
}
}
------------------------------------------------------
public CustomConversions customConversions(CryptoManager cryptoManager, ObjectMapper objectMapper) {
List<GenericConverter> newConverters = new ArrayList();
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
valueConversions.setConverterFactory(
new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap(), objectMapper));
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
configurationAdapter.setPropertyValueConversions(valueConversions);
configurationAdapter.registerConverters(newConverters);
configurationAdapter.registerConverter(OtherConverters.JsonNodeToMap.INSTANCE); // added
configurationAdapter.registerConverter(OtherConverters.MapToJsonNode.INSTANCE); // added
configurationAdapter.registerConverter(new OtherConverters.EnumToObject(getObjectMapper()));
configurationAdapter.registerConverterFactory(new IntegerToEnumConverterFactory(getObjectMapper()));
configurationAdapter.registerConverterFactory(new StringToEnumConverterFactory(getObjectMapper()));
configurationAdapter.registerConverterFactory(new BooleanToEnumConverterFactory(getObjectMapper()));
});
return customConversions;
}

Related

Using jackson deserialising a property which can be List of object or the object

My lib is calling an API which can return either of the following JSON structure -
{
"key_is_same" : {
"inner" : "val"
}
}
-or-
{
"key_is_same" : [{
"inner" : "val1"
},
{
"inner" : "val2"
}
]
}
Is there any annotation in jakson which can handle this and deserializ it into respective type
Looks like you are looking for the ACCEPT_SINGLE_VALUE_AS_ARRAY deserialization feature.
Feature that determines whether it is acceptable to coerce non-array (in JSON) values to work with Java collection (arrays, java.util.Collection) types. If enabled, collection deserializers will try to handle non-array values as if they had "implicit" surrounding JSON array. This feature is meant to be used for compatibility/interoperability reasons, to work with packages (such as XML-to-JSON converters) that leave out JSON array in cases where there is just a single element in array.
Feature is disabled by default.
It could be enabled either in ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
Or via the #JsonFormat annotation:
#JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
private List<Foo> oneOrMany;
For illustration purposes, consider the following JSON documents:
{
"oneOrMany": [
{
"value": "one"
},
{
"value": "two"
}
]
}
{
"oneOrMany": {
"value": "one"
}
}
It could be the deserialized to the following classes:
#Data
public class Foo {
private List<Bar> oneOrMany;
}
#Data
public class Bar {
private String value;
}
Just ensure the feature is enabled in your ObjectMapper or your field is annotated with #JsonFormat(with = Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY).
And in case you are looking for the equivalent feature for serialization, refer to WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED.
I would recommend using Object as your data type for the property which is dynamic. So Here is my sample.
import java.util.Arrays;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class MainObject {
private Object key_is_same;
public Object getKey_is_same() {
return key_is_same;
}
public void setKey_is_same(Object key) {
this.key_is_same = key;
}
public static class KeyObject {
private String inner;
public String getInner() {
return inner;
}
public void setInner(String inner) {
this.inner = inner;
}
}
public static void main(String...s) throws JsonProcessingException {
MainObject main = new MainObject();
KeyObject k = new KeyObject();
k.setInner("val1");
main.setKey_is_same(k);
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(main));
main.setKey_is_same(Arrays.asList(k, k));
System.out.println(om.writeValueAsString(main));
public static void main(String...s) throws IOException {
MainObject main = new MainObject();
KeyObject k = new KeyObject();
k.setInner("val1");
main.setKey_is_same(k);
ObjectMapper om = new ObjectMapper();
System.out.println(om.writeValueAsString(main));
main.setKey_is_same(Arrays.asList(k, k));
System.out.println(om.writeValueAsString(main));
// Deserialize
MainObject mainWithObject = om.readValue("{\"key_is_same\":{\"inner\":\"val1\"}}", MainObject.class);
MainObject mainWithList = om.readValue("{\"key_is_same\":[{\"inner\":\"val1\"},{\"inner\":\"val1\"}]}", MainObject.class);
if(mainWithList.getKey_is_same() instanceof java.util.List) {
((java.util.List) mainWithList.getKey_is_same()).forEach(System.out::println);
}
}
}
}
Output
{"key_is_same":{"inner":"val1"}}
{"key_is_same":[{"inner":"val1"},{"inner":"val1"}]}

Play Java validation fail for Set and Map

I use Play 2.5.12 to provide a web service to create an object and validate its attributes. Here is a simplified code of what I want to do:
public class Example {
private String lastName;
private List<String> firstNames;
private Map<String, Integer> vehicles;
public Example() {}
public String validate() {
if (vehicles.get("Ferrari") != null)
return "Liar!";
return null;
}
#Override
public String toString() {
return new ToStringBuilder(this).append(lastName).append(firstNames).append(vehicles).toString();
}
// getters and setters
}
public class ExampleController extends Controller {
private FormFactory formFactory;
#Inject
public ExampleController(FormFactory formFactory) {
this.formFactory = formFactory;
}
#BodyParser.Of(BodyParser.Json.class)
public Result createExample() {
Example exampleFromJackson = Json.fromJson(request().body().asJson(), Example.class);
System.out.println(exampleFromJackson.toString());
Example exampleFromForm = formFactory.form(Example.class).bindFromRequest().get();
System.out.println(exampleFromForm.toString());
// etc
return created();
}
}
If I call the web service with a body like this:
{
"lastName": "Martin",
"firstNames": ["Robert", "Cecil"],
"vehicles": {
"BMW": 1,
"Seat": 1
}
}
The deserialized object by jackson prints correctly then this error occurs:
org.springframework.beans.InvalidPropertyException: Invalid property 'firstNames[0]' of bean class [models.Example]: Property referenced in indexed property path 'firstNames[0]' is neither an array nor a List nor a Map; returned value was [[]]
If I replace the Set by a List, I can avoid this error and then I obtain the following:
models.Example#68d2162a[Martin,[Robert, Cecil],{BMW=1, Seat=1}]
models.Example#29ef1c11[Martin,[Robert, Cecil],{}]
The deserialized object by jackson is correct but the one obtained by the request isn't so I can't do a correct validation.
So my questions are:
- Why Play doesn't let me use a Set?
- Why is the Map not retrieved?

Spring websocket #messagemapping de-serialization issue java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast

I am writing a spring websocket application with StompJS on the client side.
On the client side I am intending to send a List of objects and on the server side when it is mapping into java object, it converts itself into a LinkedHashMap
My client side code is
function stomball() {
stompClient.send("/brkr/call", {}, JSON.stringify(listIds));
}
Listids looks like
[{
"path": "/a/b/c.txt",
"id": 12
}, {
"path": "/a/b/c/d.txt",
"id": 13
}]
List Id object looks like
public class ListId {
private String path;
private Long id;
//getters and setters...
}
The Controller looks like this
#MessageMapping("/call" )
#SendTo("/topic/showResult")
public RetObj process(List<ListId> listIds) {
if (!listIds.isEmpty()) {
for(ListId listId: listIds) {
}
}
So I get a java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.blah.ListId
However when I do the same with normal Spring Controller with RestMapping it works fine, Is there anything with springs MessageMapping annotation that maps objects to java differently than the traditional way
I am not sure why is not casting to ListID
I changed it from a List to an Array and it works! Here is what I did
#MessageMapping("/call" )
#SendTo("/topic/showResult")
public RetObj process(ListId[] listIds) {
if (!listIds.isEmpty()) {
for(ListId listId: listIds) {
}
}
Thanks to this question ClassCastException: RestTemplate returning List<LinkedHashMap> instead of List<MymodelClass>
I know this question has already been answered but here's another solution.
To get Jackson to convert your JSON array to list you'll have to wrap it in another object and serialize/deserialize that object.
So you'll have to send following JSON to server
{
list: [
{
"path": "/a/b/c.txt",
"id": 12
}, {
"path": "/a/b/c/d.txt",
"id": 13
}
]
}
List is wrapped into a another object.
Following is the wrapper class
class ServiceRequest {
private List<ListId> list;
public List<ListId> getList() {
if (list == null) {
list = new ArrayList<ListId>();
}
return list;
}
}
and the message method will become
#MessageMapping("/call" )
#SendTo("/topic/showResult")
public RetObj process(ServiceRequest request) {
List<ListId> listIds = request.getList();
if (!listIds.isEmpty()) {
for(ListId listId: listIds) {
}
}
}
Test Code
import java.util.ArrayList;
import java.util.List;
import org.codehaus.jackson.map.ObjectMapper;
public class TestJackson {
public static void main(String[] args) throws Exception {
System.out.println("Started");
String json = "{\"list\":[{\"path\":\"/a/b/c.txt\",\"id\":12},{\"path\":\"/a/b/c/d.txt\",\"id\":13}]}";
ObjectMapper mapper = new ObjectMapper();
ServiceRequest response = mapper.readValue(json.getBytes("UTF-8"), ServiceRequest.class);
for(ListId listId : response.getList()) {
System.out.println(listId.getId() + " : " + listId.getPath());
}
}
public static class ServiceRequest {
private List<ListId> list;
public List<ListId> getList() {
if (list == null) {
list = new ArrayList<ListId>();
}
return list;
}
}
public static class ListId {
private String path;
private String id;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
}
Test Output
Started
12 : /a/b/c.txt
13 : /a/b/c/d.txt

Jackson, move dynamic property value one level up

I have problem with modelling server responses, some of them look like that:
{
"_links":{
"self":{
"href":"http:\/\/example.com"
}
},
"_embedded":{
"category":{
<...data...>
}
}
}
or
{
"_links":{
"self":{
"href":"http:\/\/example.com"
}
},
"_embedded":{
"episodes":[
<...list_data...>
]
}
}
It seems that "_embedded" property has only one JSON object and that object has only one property ( named differently ) with actual data.
I would like to create some kind of generic POJO class to support those kind of responses, something like:
public abstract class EmbeddedResponse<T> {
#JsonProperty("_embedded")
private T embedded;
public T getEmbedded() {
return embedded;
}
... <other_members> ...
}
public class CategoriesResponse extends EmbeddedResponse<List<Category>> {
}
Where calling 'getEmbedded()' would return list of categories ( or episodes, or anything ).
I am working with custom deserialization now, but without much success, I would like to keep code base minimal.
Solution, abstract POJO class:
public class EmbeddedResponse<T> {
#JsonProperty("_embedded")
#JsonDeserialize( using = EmbeddedResponseDeserializer.class )
private T embedded;
public T getEmbedded() {
return embedded;
}
}
POJO for actual response:
public class CategoriesResponse extends EmbeddedResponse<List<Category>> {
}
Deserializer for JSON in question:
public class EmbeddedResponseDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer {
private JavaType javaType;
#Override
public Object deserialize( JsonParser jsonParser, DeserializationContext ctxt ) throws IOException {
ObjectCodec objectCodec = jsonParser.getCodec();
JsonNode node = objectCodec.readTree(jsonParser);
// Get first it might require correction
String fieldName = node.fieldNames().next();
JsonNode skippedNode = node.get( fieldName );
return objectCodec.readValue( skippedNode.traverse(), javaType );
}
#Override
public JsonDeserializer<?> createContextual( DeserializationContext ctxt, BeanProperty property ) throws JsonMappingException {
javaType = property.getType();
return this;
}
}
It might require more tweeks but at this point this solution is working
I would use the Java 8 Optional object when modelling the objects. This way you get a flexible model and nice programming model by e.g. using the ifPresent-method.
So, the root class could be modelled along the lines of:
public class Response {
private Embedded embedded;
private Links links;
#JsonCreator
public Response(
#JsonProperty("_links") final Links links,
#JsonProperty("_embedded") final Embedded embedded) {
this.links = links;
this.embedded = embedded;
}
public Embedded embedded() {
return embedded;
}
public Links links() {
return links;
}
}
The object that defines the embedded content (i.e. category or episodes) could be modelled like this:
public class Embedded {
private final Category category;
private final List<Episode> episodes;
#JsonCreator
public Embedded(
#JsonProperty("episodes") final List<Episode> episodes,
#JsonProperty("category") final Category category) {
this.episodes = episodes;
this.category = category;
}
public Optional<Category> category() {
return Optional.ofNullable(category);
}
public Optional<List<Episode>> episodes() {
return Optional.ofNullable(episodes);
}
}
When programming towards these objects the following pattern could be used:
final InputStream resource = ...; // retrieve a stream somehow
// Map the stream to the response object
final Response response = new ObjectMapper().readValue(resource, Response.class);
// Use the Optional-style for processing category data
response.embedded().category().ifPresent(category -> {
// do category stuff with the Category-object
});
// Once more, use the Optional-style - this time for processing episodes data
response.embedded().episodes().ifPresent(episodes -> {
// do episodes stuff with the List of Episodes
});

Convert JSON to Object with JAXB only if all fields are filled out

I'm building a RESTful web service with Jersey. I use JAXB to convert incoming JSON objects into Java objects. Unfortunately this approach allows to create Java objects which don't have all mandatory fields. If I have 3 mandatory fields but the JSON contains only 1 field, I would like to see an exception thrown.
Resource class:
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
public class Resource {
private int field1;
private String field2;
private String field3;
public Resource() {
}
...
}
REST class:
#Path("resource")
public class ResourceREST {
...
#POST
#Consumes(APPLICATION_JSON)
#Produces(TEXT_PLAIN)
public String createResource(Resource resource) {
...
}
...
}
Is there any possibility to do this with JAXB? If not, how can I realize this input validation?
Thanks in advance!
I have gone through the same scenario and applied some logic to fix this after the JSON is generated.
In a List add those Field Names that you considered as mandatory.
public static final List<String> REQUIRED_FIELDS = new ArrayList<String>();
static {
REQUIRED_FIELDS.add("Field1");
REQUIRED_FIELDS.add("Field2");
};
Send those JSON that you have build to a validate method.
Your validate method should be like this.
public void validateRequiredFields(JSONObject jsonObject, List<String> requiredFields) throws ParserException, Exception {
if (log.isDebugEnabled()) {
log.debug("Entering validateForRequiredFields");
}
List<String> missingFields = new ArrayList<String>();
try {
if (requiredFields != null) {
for (String requiredField : requiredFields) {
if (ifObjectExists(jsonObject, requiredField)) {
if (StringUtils.isEmpty(jsonObject.getString(requiredField))) {
missingFields.add(requiredField);
}
} else {
missingFields.add(requiredField);
}
}
}
if (missingFields != null && missingFields.size() > 0) {
throw new Exception(missingFields);
}
} catch (JSONException e) {
throw new ParserException("Error occured in validateRequiredFields", e);
}
}

Categories