I would like to implement a custom deserializer for our REST API that is not only used by Java application. Therefore I don't want to have Jackson putting type information into the serialized JSON.
I'm currently struggling with deserializing CollectionExpand since it contains a list data of specific ResourceModel.
public class EntityModel<R extends ResourceModel> implements Serializable {
private R data;
private List<ResourceLink> links;
private List<CollectionExpand> expands;
}
public class CollectionExpand {
private String name;
// Resource Model is an interface
private Collection<ResourceModel> data;
}
ResourceModel is an interface an each CollectionExpand contains a collection of one type of ResourceModel per name.
For example a json output could look like this.
{
"data": {},
"links": [],
"expand": [
{
"name": "photos",
"data": [
{
"id": 12,
"name": "hello.jpg"
},
{
"id": 12,
"name": "hello.jpg"
}
]
},
{
"name": "persons",
"data": [
{
"id": 783378,
"name": "Peter",
"age": 12
},
{
"id": 273872,
"name": "Maria",
"age": 77
}
]
}
]
}
As you can see each name contains the same type of resource model. photos contains PhotoResourceModel and person contains PersonResourceModel.
I started to implement my custom Jackson Deserializer
public class CollectionExpandDeserializer extends StdDeserializer<CollectionExpand> {
public CollectionExpandDeserializer() {
super(CollectionExpand.class);
}
#Override
public CollectionExpand deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
CollectionExpand collectionExpand = new CollectionExpand();
if (Objects.equals(p.nextFieldName(), "name")) {
collectionExpand.setName(p.nextTextValue());
}
if (Objects.equals(p.nextFieldName(), "data")) {
// depending on the field name I would like to delegate the deserialization to a specific type.
if (name.equals("photos") {
// how to do this?
collectionExpand.setData(/* deserialize to a list of PhotoResource */);
}
}
return collectionExpand;
}
I'm current stuck on how can I delegate telling Jackson to deserialize this as a PhotoResource list.
In general is this the right approach or is there another way to do it (without putting any Jackson meta data into the JSON while serialization)?
I have ended up implementing my custom deserializer as below
#Override
public CollectionExpand deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
JsonNode node = ctx.readTree(p);
CollectionExpand collectionExpand = new CollectionExpand();
collectionExpand.setName(node.get("name").asText());
ArrayNode data = node.withArray("data");
Iterator<JsonNode> iterator = data.iterator();
while (iterator.hasNext()) {
Class<? extends ResourceModel> aClass = resolveClass(collectionExpand.getName());
if (aClass != null) {
JsonNode jsonNode = iterator.next();
collectionExpand.getData().add(p.getCodec().treeToValue(jsonNode, aClass));
}
}
return collectionExpand;
}
private Class<? extends ResourceModel> resolveClass(String name) {
if ("contents".equals(name)) {
return ContentsGetResourceModel.class;
} else if ("tags".equals(name)) {
return TagsGetResourceModel.class;
} else {
return null;
}
}
I took my a while to understand how to deserialize a JsonNode/TreeNode into a specific type. In the end I learned that this can be basically done by using the parsers codec.
PhotoResource photoResource = p.getCodec().treeToValue(jsonNode, PhotoResource.class);
Related
I have a JSON object that one of the a attributes is a JSON string.
a = {
"dt" : "2022-01-02 00:00:00"
"fx": "{\"id\":1,\"faixaId\":1,\"speedAtivo\":true}",
"hash": "8c91a61a0a49b73de2fc13caed00e6a93dbe435b354216802da0dbe8bfda3300",
}
In JavaScript, I can convert the "fx" attribute to an object using:
a.fx = JSON.parse(a.fx)
And the new JSON:
a = {
"dt" : "2022-01-02 00:00:00"
"fx": {"id":1,
"faixaId":1,
"speedAtivo":true
},
"hash": "8c91a61a0a49b73de2fc13caed00e6a93dbe435b354216802da0dbe8bfda3300",
}
There is a way to do this with Java?
Yes, you can parse JSON string using one of these libraries
to use these library check the docs and
maven dependency
But make sure that your JSON is in correct format as the above JSON is missing comma after the first line ending.
Below is the simple example to parse a JSON String using the above library.
String jsonStr = "{\"dt\":\"2022-01-02 00:00:00\",
\"fx\":\"id\":1,\"faixaId\":1,\"speedAtivo\":true},
\"hash\":\"8c91accsiamkFVXtw6N7DnE3QtredADYBYU35b354216802da0dbe8bfda3300\",
}";
JSONObject strObj = JSONObject.fromObject(jsonStr);
Output:
{
"dt": "2022-01-02 00:00:00",
"fx": {
"id": 1,
"faixaId": 1,
"speedAtivo": true
},
"hash":
"8c91accsiamkFVXtw6N7DnE3QtredADYBYU35b354216802da0dbe8bfda3300"
}
If you use Jackson lirary to convert objects from JSON String to objects via custom deserializer.
First you create Classes that will represent the main object and fx object
public class AClass {
public String dt;
#JsonDeserialize(using = Deserializer.class)
public FxClass fx;
public String hash;
}
public class FxClass {
public int id;
public int faixaId;
public boolean speedAtivo;
}
Then you create deserizalizer
public class Deserializer extends StdDeserializer<FxClass> {
protected Deserializer(Class<?> vc) {
super(vc);
}
protected Deserializer() {
this(null);
}
#Override
public FxClass deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
TextNode treeNode = jsonParser.getCodec().readTree(jsonParser);
return new ObjectMapper().readValue(treeNode.asText(), FxClass.class);
}
}
And add deserializer to ObjectMapperConfiguration
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(FxClass.class, new Deserializer());
mapper.registerModule(module);
And thats it. To convert JsonString to object
AClass readValue = new ObjectMapper.readValue(json, AClass.class);
For additional info try reading https://www.baeldung.com/jackson-deserialization
I have a JSON that looks more or less like this:
{
"modules": {
"someExistingModule": {
"name": "pug",
...
},
"randomExistingModule": {
"type": "cat",
...
},
"myNewModule": { // <----- I care about this module. Note that this is NOT an array
"modules": {
"img1": {
"type": "image",
"url": "https://some/random/image,
"altText": "Some image description
},
"img2": {
"type": "image",
"url": "https://some/random/image,
"altText": "Some image description
},
"img3": {
"type": "image",
"url": "https://some/random/image,
"altText": "Some image description
},
"txt1": { // <------ Updated JSON
"type": "text",
"content": "Hello world 1"
},
"txt2": {
"type": "text",
"content": "Hello world 2"
},
...
}
}
Inside myModule there can be N number of imgN objects and txtN. I need to parse this dynamically.
My current Response class looks like this:
public class MyModuleResponse extends SomeResponseClass
{
#Override
public void parse(InputStream data)
{
T responseBody = readJsonStream(data, MyModuleResponseBody.class());
MyModuleDataParser.parse(responseBody);
}
MyModuleDataParser.java
...
public static MyModuleDataParser parse(#Nullable MyModuleResponseBody body)
{
parseSomeExistingModule();
parseRandomExistingModule();
parseMyNewModule(); // <--- This is the new module I'm trying to parse. Currently, this method is empty.
}
MyModuleResponseBody.java
public class MyModuleResponseBody
{
public Modules modules;
public static class Modules
{
SomeExistingModule someExistingModule;
RandomExistingModule randomExistingModule;
MyNewModule myNewModule; // <----- My new module
}
public static class SomeExistingModule
{
String name;
...
}
public static class RandomExistingModule
{
String type;
...
}
public static class MyNewModule
{
public ??? modules; // <--- Trying to define the Type here. Something like List<MyImageModule>. But, it won't work
}
MyImageModule.java
public class MyImageModule extends Module // <---- Update: This class now extends a generic Module class
{
private String url;
private String altText;
}
MyTextModule.java <---- New Module
public class MyTextModule extends Module // New class
{
private String content;
}
Module.java
public class Module // <----- New parent class
{
protected String type;
}
How do I create a list of MyImageModule from myNewModule? I believe I need to use some kind of TypeAdapter from Gson library. But, I'm not familiar how to do this inside an existing response.
Use Map<String, MyImageModule>, in fact, a hashmap to solve the issue of non-list modules object in the json.
public static class MyNewModule {
public Map<String, MyImageModule> modules; // initialize as a hashmap
}
I'm taking entity with child presented as numeric child_entity_id. But sometimes instead of child_entity_id I have to handle child_entity_selector json object instead. Which is sent for extracting the entity by some values from a database.
Parent entity has such annotations
#Entity
class Parent {
#Id
long id;
#JsonDeserialize(using = EntityReferenceDeserializer.class)
#JsonSerialize(using = EntityReferenceSerializer.class)
#JsonProperty("child_entity_id")
#JsonAlias("child_entity_selector")
Child child;
For response json like
{
"id" : 1,
"child_entity_id": 1,
"child_entity_selector": {
"child_name": "I am a child"
},
}
An exception should be thrown because only "child_entity_id" or "child_entity_selector" have to be simultaneously in one json.
When I receive such json, it parses correctly, but my deserializer handles both of child_entity_id and child_entity_selector and the last one remains in the result.
Currently I'm trying to obtain both of source json and entity to check that json has not duplicated references covered by aliases.
I've set up a SimpleModule and the custom deserializer
#Component
public class WebConfig {
#Bean
public SimpleModule simpleModule() {
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == Child.class)
return new ChildDeserializer(deserializer);
return deserializer;
}
});
return module;
}
}
and deserializer
public class ChildDeserializer extends StdDeserializer<Child> implements ResolvableDeserializer {
private final JsonDeserializer<?> defaultDeserializer;
public ChildDeserializer(JsonDeserializer<?> defaultDeserializer) {
super(Child.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override
public Child deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Child modelExecution = (Child) defaultDeserializer.deserialize(p, ctxt);
// Can not correctly read raw json after parsing -- node becomes null now after deserialization above
JsonNode node = p.getCodec().readTree(p);
boolean duplication = node.has("child_entity_id") && node.has("child_entity_selector");
if (duplication) {
throw new RuntimeException("Duplicated!")
} else {
log("Not duplicated");
}
return child;
}
#Override
public void resolve(DeserializationContext ctxt) throws JsonMappingException {
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
}
After the deserialization I can not access the JsonNode value. And vice versa after parsing JsonNode value can not obtain deserialized Child entity.
Expected behavior on
{
"id" : 1,
"child_entity_id": 1,
}
and
{
"id" : 1,
"child_entity_selector": {
"child_name": "I am a child"
},
}
is correctly parsed entity.
But on both of references
{
"id" : 1,
"child_entity_id": 1,
"child_entity_selector": {
"child_name": "I am a child"
},
}
an exception should be thrown.
You could handle it with a #JsonCreator in the Parent class
#JsonCreator
public Parent(#JsonProperty("child_entity_id") Child childFromId, #JsonProperty("child_entity_selector") Child childFromSelector) {
if (childFromId != null && childFromSelector != null) {
throw new RuntimeException("Duplicated!");
}
this.child = (childFromSelector == null) ? childFromId : childFromSelector;
}
There is a JsonParser.Feature.STRICT_DUPLICATE_DETECTION if you also need to check for duplicate fields:
{
"id" : 1,
"child_entity_id": 1,
"child_entity_id": 2
}
Unfortunately it doesn't work with aliases
I have a class that looks like
public class ActiveDirectorySetup implements Serializable {
private ActiveDirectoryDataSource activeDirectoryDataSource;
private Optional<ShActiveDirectorySettings> shActiveDirectorySettings;
private Optional<SaActiveDirectorySettings> saActiveDirectorySettings;
// ...
}
I send this over the API as
Optional<ActiveDirectoryConfiguration> configuration = store.getConfiguration();
if (configuration.isPresent()) {
return configuration.get();
}
What I see on the browser is
[
{
"activeDirectoryDataSource":{
"host":"localhost",
"port":0,
"userName":"user",
"password":"password",
"activeDirectoryQueryConfig":{
"base":{
"present":false
},
"filter":"filter",
"attributes":[
]
},
"activeDirectorySslSettings":{
"present":false
}
},
"shActiveDirectorySettings":{
"present":true
},
"saActiveDirectorySettings":{
"present":true
}
}
]
for a payload that looks like
{
"activeDirectorySetups": [
{
"activeDirectoryDataSource": {
"host": "localhost",
"port": 0,
"userName": "user",
"password": "password",
"activeDirectoryQueryConfig": {
"base": null,
"filter": "filter",
"attributes": []
},
"activeDirectorySslSettings": null
},
"shActiveDirectorySettings": {
"enableUserMapping": true,
"attributes": null
},
"saActiveDirectorySettings": null
}
]
}
As you could see, I get {"present":true} instead of the actual value.
I am using jackson-datatype-jdk8 for this work. How can I force it to replace {"present":true} with actual values - either null or
{"enableUserMapping": true, "attributes": null}
I'm pretty sure you'd need to write custom serialization / deserialization functionality for this.
Deserializer
public class OptionalDeserializer<T> extends StdDeserializer<Optional<T>> {
private ObjectMapper customObjectMapper;
private Class<T> type;
/**
* #param customObjectMapper any ObjectMapper, possibly with deserialization logic for the wrapped type
* #param type the wrapped type
*/
public OptionalDeserializer(ObjectMapper customObjectMapper, Class<T> type) {
this(Optional.class);
this.customObjectMapper = customObjectMapper;
this.type = type;
}
// At least one type-based constructor is required by Jackson
private OptionalDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Optional<T> deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
// Read entire tree
JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser);
// Check if "present" is true
if (treeNode.has("present") && treeNode.get("present").asBoolean()) {
// Read your wrapped value
return Optional.of(customObjectMapper.treeToValue(treeNode.get("data"), type));
}
// Return empty() by default
return Optional.empty();
}
}
Serializer
Note you could include a custom ObjectMapper for the Box type anywhere in the pipeline. It's omitted in the serializer for simplicity.
public class OptionalSerializer<T> extends StdSerializer<Optional<T>> {
public OptionalSerializer(Class<T> type) {
this(TypeFactory.defaultInstance().constructParametricType(Optional.class, type));
}
protected OptionalSerializer(JavaType javaType) {
super(javaType);
}
#Override
public void serialize(Optional<T> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
if (value.isPresent()) {
gen.writeBooleanField("present", true);
gen.writeObjectField("data", value.get());
} else {
gen.writeBooleanField("present", false);
}
gen.writeEndObject();
}
}
Example usage:
public static void main(String[] args) throws IOException {
ObjectMapper optionalMapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
// Add any custom deserialization logic for Box objects to this mapper
ObjectMapper boxMapper = new ObjectMapper();
OptionalDeserializer<Box> boxOptionalDeserializer = new OptionalDeserializer<>(boxMapper, Box.class);
OptionalSerializer<Box> boxOptionalSerializer = new OptionalSerializer<>(Box.class);
module.addDeserializer(Optional.class, boxOptionalDeserializer);
// use addSerializer(JsonSerializer<?> ser), not addSerializer(Class<? extends T> type, JsonSerializer<T> ser)
// The generic types involved here will otherwise not let the program compile
module.addSerializer(boxOptionalSerializer);
optionalMapper.registerModule(module);
String json = "{\"present\": true, \"data\": {\"myValue\": 123}}";
Optional optional = optionalMapper.readValue(json, Optional.class);
#SuppressWarnings("unchecked") // Guaranteed safe cast
Optional<Box> boxOptional = (Optional<Box>) optional;
// Prints "123"
boxOptional.ifPresent(box -> System.out.println(box.getMyValue()));
// Prints the contents of "json" (variable defined above)
System.out.println(optionalMapper.writeValueAsString(boxOptional));
}
Where Box is just a simple example class:
private static class Box {
private int myValue;
public int getMyValue() {
return myValue;
}
public void setMyValue(int myValue) {
this.myValue = myValue;
}
}
I think you are relying on default java serialization while using Optional in Java 8.
Kindly note that Optional is not serializable and hence, you will have to write your own JSON serializer/deserializer.
I am working on a Spring Boot application. In the html view, I make a ajax call to a RestController, which returns a list of custom entities:
#Controller
public class MyController {
#ResponseBody
#JsonView(View.MyView.class)
public List<CustomEntity> getEntities() {
...
}
}
This is working fine, I am getting, as expected, following structure:
{
"id": "1",
"name": "Test1"
},
{
"id": "2",
"name": "Test2"
}
In the view, I want to use it with Dynatable. And here comes my problem. I need following structure:
{
"records": [
{
"id": "1",
"name": "Test1"
},
{
"id": "2",
"name": "Test2"
}
],
"queryRecordCount": 2,
"totalRecordCount": 2
}
Is there a way to generate the JSOn view using jackson (or any other framework) based on a template, so I can use the data with Dynatable, and if so, how?
Thanks in advance,
Stephan
You could create a wrapper that does this for you...
class DyntableResponse<T> {
private List<T> records;
public List<T> getRecords() { return records; }
public void setRecords(final List<T> records) { this.records = records; }
public int getQueryRecordCount() { return records.size(); }
public int getTotalRecordCount() { return records.size(); }
}
And then return it from your RestController...
#Controller
public class MyController {
#ResponseBody
#JsonView(View.MyView.class)
public DyntableResponse<CustomEntity> getEntities() {
final DyntableResponse<CustomEntity> resp = new DyntableResponse<>();
resp.setRecords(...); // Your finder here.
return resp;
}
}
This is untried, but should be close.