Spring GraphQL - how to map JSON extended scalar to POJO attribute? - java

I am implementing GraphQL API using Spring for GraphQL project and the GraphQL Java Extended Scalars project for handling JSON attributes since the data attribute I have is dynamic and its structure is not known by the server.
This JSON attribute is part of the payload in a mutation input, which POJO looks like this:
#Accessors(fluent = true)
#Builder
#Data
#JsonDeserialize(builder = MutationInputModel.MutationInputModelBuilder.class)
public class MutationInputModel {
private JsonNode data;
...
}
As exemplified above, I am using Lombok annotations for deserialization and accessors generation.
The data attribute is Jackson's JsonNode type.
However, when calling this mutation I'm getting the following exception:
org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.fasterxml.jackson.databind.JsonNode]: Is it an abstract class?; nested exception is java.lang.InstantiationException
On a previous GraphQL API implementation using this library this same model would work just fine.
My question is how to properly setup JSON scalar type on the POJOs?
I have tried Jackson's ObjectNode and Map<String, Object> with no luck neither.

I ended up implementing my own custom coercing approach so I'll share here for further reference:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.Map;
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import static graphql.scalars.ExtendedScalars.Object;
/**
* Custom Coercing implementation in order to parse GQL Objects,
* which get mapped as LinkedHashMap instances by {#link graphql.scalars.object.ObjectScalar}
* into Jackson's JsonNode objects.
*/
public class JsonNodeCoercing implements Coercing<JsonNode, Object> {
private static final Coercing<?, ?> COERCING = Object.getCoercing();
private final ObjectMapper objectMapper;
public JsonNodeCoercing(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
#Override
public Object serialize(Object input) throws CoercingSerializeException {
return input;
}
#Override
public JsonNode parseValue(Object input) throws CoercingParseValueException {
return objectMapper.valueToTree(input);
}
#Override
public JsonNode parseLiteral(Object input) throws CoercingParseLiteralException {
return parseLiteral(input, Collections.emptyMap());
}
#Override
public JsonNode parseLiteral(Object input, Map<String, Object> variables) throws CoercingParseLiteralException {
return objectMapper.valueToTree(COERCING.parseLiteral(input, variables));
}
}
And then this is how the scalar bean config looks like:
#Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer(ObjectMapper objectMapper) {
GraphQLScalarType jsonScalarType = GraphQLScalarType.newScalar()
.name("JSON")
.description("A JSON scalar")
.coercing(new JsonNodeCoercing(objectMapper))
.build();
return wiringBuilder -> wiringBuilder
.scalar(jsonScalarType);
}
If anyone has alternate/simpler approach please share.

Related

Flat JSON in Jackson for class/record/value object with one field

I have a Java record with one field only:
public record AggregateId(UUID id) {}
And a class with the AggregateId field (other fields removed for readability)
public class Aggregate {
public final AggregateId aggregateId;
#JsonCreator
public Aggregate(
#JsonProperty("aggregateId") AggregateId aggregateId
) {
this.aggregateId = aggregateId;
}
}
The implementation above serialize and deserialize JSON with given example:
ObjectMapper objectMapper = new ObjectMapper();
String content = """
{
"aggregateId": {
"id": "3f61aede-83dd-4049-a6ff-337887b6b807"
}
}
""";
Aggregate aggregate = objectMapper.readValue(content, Aggregate.class);
System.out.println(objectMapper.writeValueAsString(aggregate));
How could I change Jackson config to replace JSON by that:
{
"aggregateId": "3f61aede-83dd-4049-a6ff-337887b6b807"
}
without giving up a separate class for AggregateId and access through fields, without getters?
I tried #JsonUnwrapper annotation, but this caused throws
Exception in thread "X" com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Invalid type definition for type `X`:
Cannot define Creator parameter as `#JsonUnwrapped`: combination not yet supported at [Source: (String)"{
"aggregateId": "3f61aede-83dd-4049-a6ff-337887b6b807"
}"
or
Exception in thread "X" com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot define Creator property "aggregateId" as `#JsonUnwrapped`:
combination not yet supported at [Source: (String)"{
"aggregateId": "3f61aede-83dd-4049-a6ff-337887b6b807"
}"
Jackson version: 2.13.1
dependencies {
compile "com.fasterxml.jackson.core:jackson-annotations:2.13.1"
compile "com.fasterxml.jackson.core:jackson-databind:2.13.1"
}
Of course, it's possible with a custom serializer/deserializer, but I'm looking for an easier solution because I have many different classes with a similar issue.
The combination of #JsonUnwrapped and #JsonCreator is not supported yet, so we can generate a solution like this:
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.UUID;
public class AggregateTest {
static record AggregateId(#JsonProperty("aggregateId") UUID id) {}
static class Aggregate {
#JsonUnwrapped
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
public final AggregateId _aggregateId;
public final String otherField;
#JsonCreator
public Aggregate(#JsonProperty("aggregateId") UUID aggregateId,
#JsonProperty("otherField") String otherField) {
this._aggregateId = new AggregateId(aggregateId);
this.otherField = otherField;
}
}
public static void main(String[] args) throws JsonProcessingException {
String rawJson =
"{\"aggregateId\": \"1f61aede-83dd-4049-a6ff-337887b6b807\"," +
"\"otherField\": \"İsmail Y.\"}";
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
Aggregate aggregate = objectMapper
.readValue(rawJson, Aggregate.class);
System.out.println(objectMapper
.writeValueAsString(aggregate));
}
}
Here we briefly get rid of the #JsonUnwrapped field.
We get the UUID with the name aggregateId and create an AggregateId record.
Detailed explanations about it:
https://github.com/FasterXML/jackson-databind/issues/1467
https://github.com/FasterXML/jackson-databind/issues/1497

How to tell jackson to ignore some fields when deserialising

I am calling an endpoint in which it returns an object. In this object it contains some fields and also a field of another type of object.
E.g.
class ResponseObject{
private final boolean success;
private final String message;
private final DifferentType different;
}
I am calling the endpoint via RestTemplate:
private LogonResponseMessage isMemberAuthenticated(UserCredentialDomain userCredentialDomain)
{
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject(
"http://abc.local:8145/xyz/member/authenticate/{memberLoginName}/{password}", ResponseObject.class,
userCredentialDomain.getUsername(), userCredentialDomain.getPassword());
}
So my consuming app is giving this error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `fgh.thg.member.DifferentTypeABC` (no Creators, like default constructor, exist): cannot deserialize from Object v
alue (no delegate- or property-based Creator)
I am aware that it's telling me to put a default constructor in the DifferentTypeABC class in order for jackson to deserialise it but i can't do that easily as I'll need to update the dependency on apps that the DifferentTypeABC is in across several different repos.
So i was wondering if there is a way of configuring RestTemplate or jackson on the consuming app so that it ignores attempting to deserialise objects if it doesn't contain a default constructor? To be honest, I am pretty much interested in the success and message fields on the response object.
Another way to solve the problem is by enhancing DifferentType in this module using Jackson creator Mixin. Below is an example:
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import java.io.IOException;
public class Test {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(mapper.getVisibilityChecker()
.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
.withGetterVisibility(JsonAutoDetect.Visibility.NONE)
.withSetterVisibility(JsonAutoDetect.Visibility.NONE)
.withCreatorVisibility(JsonAutoDetect.Visibility.NONE));
mapper.addMixIn(DifferentType.class, DifferentTypeMixin.class);
String raw = "{\"message\": \"ok\", \"differentType\": {\"name\": \"foo bar\"}}";
ResponseObject object = mapper.readValue(raw, ResponseObject.class);
System.out.println(mapper.writeValueAsString(object));
}
#Data
static class ResponseObject {
private String message;
private DifferentType differentType;
}
static class DifferentType {
private String name;
public DifferentType(String name) {
this.name = name;
}
}
public static abstract class DifferentTypeMixin {
#JsonCreator
DifferentTypeMixin(#JsonProperty("name") String name) {
}
}
}

How to convert a JsonNode instance to an actual pojo

At a certain point in my code, I have parse a JSON document, represented as a string, to a JsonNode, because I don't know yet the actual target pojo class type.
Now, some time later, I know the Class instance of the pojo and I want to convert this JsonNode to an actual pojo of that class (which is annotated with the proper #JsonProperty annotations). Can this be done? If so, how?
I am working with Jackson 2.10.x.
In this case you can use two methods:
treeToValue
convertValue
See below example:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.util.StringJoiner;
public class JsonNodeConvertApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(jsonFile);
System.out.println(mapper.treeToValue(node, Message.class));
System.out.println(mapper.convertValue(node, Message.class));
}
}
class Message {
private int id;
private String body;
// getters, setters, toString
}
Above code for JSON payload like below:
{
"id": 1,
"body": "message body"
}
prints:
Message[id=1, body='message body']
Message[id=1, body='message body']

How to not create many classes just for serialising nested JSON payload?

I've JSON message coming in from rabbitmq and has the following format:
{
messageId: 123,
content: {
id: "P123456",
status: false,
error: {
description: "Something has gone wrong",
id: 'E400'
}
}
}
As you can see, the message has a few nested objects within them.
When this message comes in, I will serialise it using Jackson. Right now, however, I have to create multiple classes just for one single message.
In the example message above, I have to create 3 classes just for serialising and transforming it into a class MainMessage, like so:
public class MainMessage {
private int messageId;
private MessageContentObject content;
// getters/setters...
}
public class MessageContentObject {
private String id;
private boolean status;
private MessageErrorObject error;
// getters/setters...
}
public class MessageErrorObject {
private String description;
private string id;
// getters/setters...
}
This feels very cumbersome because in some of the messages, the nesting can be pretty deep and I will have to create a lot of classes just for the purpose of having the JSON payload transformed into the MainMessage class object. The MessageContentObject and MessageErrorObject are mostly redundant because I will never use the classes directly anywhere else in the code. I would still the values in them through MainMessage though, for example:
#RabbitListener
public void consumeMessage(MainMessage msg) {
System.out.println(msg.getContent().getError().getDescription());
}
I'm using Spring with Spring Boot.
Is this really the only way I can do when it comes to dealing with nested JSON payloads?
First, when the message comes in you Deserialize it.
Now, if you don't want to create the whole data structure to look like your incoming JSON, you can go for a Map like this
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonCreator;
import java.util.Collections;
import java.util.Map;
public class Message {
private final Map<String, Object> details;
#JsonCreator // Deserialize the JSON using this creator
public Message(final Map<String, Object> details) {
super();
this.details = details;
}
#JsonAnyGetter // Serialize this class using the data from the map
public Map<String, Object> getDetails() {
return Collections.unmodifiableMap(details);
}
}
In this way, you won't need to change your Message class every time your incoming JSON changes.
However, this approach is useful only when you'll not be manipulating the data too much.
if you don't want to create the whole data structure or you only need a small portion of the response recieved at a time you can use Jackson JsonNode
import com.fasterxml.jackson.databind.JsonNode;
public class Message {
private JsonNode messageDetails;
/**
* Constructor used to transform your object into jsonNode.
*
* #param messageDetailsResponse
*/
public Message(Object messageDetailsResponse) {
this.messageDetails = JsonUtils.getNode(messageDetailsResponse);
}
//Simply create getters for accessing the data you want when you want it
public String getId() {
return messageDetails.findValue("messageId").asText();
}
//You can also use it later to map a portion of response to an model class
public Content getContent(){
return JsonUtils.fromJsonNode(messageDetails.get("content"),Content.class)
}
}
The Code for JsonUtils
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
#Slf4j
final public class JsonUtils {
/**
* The Object Mapper constant to deal with JSON to/from conversion activities.
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static JsonNode getNode(Object anyObject) {
try {
return OBJECT_MAPPER.readTree(OBJECT_MAPPER.writeValueAsBytes(anyObject));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
public static <T> T fromJsonNode(final JsonNode node, final Class<T> clazz)
throws JsonProcessingException {
return OBJECT_MAPPER.treeToValue(node, clazz);
}
Some suggestions:
Consider using Lombok to reduce the code you need to write (getters setters etc.) - will make creating classes less of a problem.
Consider using static inner classes - will mean you need less files.
If you still prefer to not write your classes down, you can deserealise to a String and traverse it using Gson or similar libraries.

JsonMappingException when serializing avro generated object to json

I used avro-tools to generate java classes from avsc files, using:
java.exe -jar avro-tools-1.7.7.jar compile -string schema myfile.avsc
Then I tried to serialize such objects to json by ObjectMapper,
but always got a JsonMappingException saying "not an enum" or "not a union".
In my test I create the generated object using it's builder or constructor.
I got such exceptions for objects of different classes...
Sample Code:
ObjectMapper serializer = new ObjectMapper(); // com.fasterxml.jackson.databind
serializer.register(new JtsModule()); // com.bedatadriven.jackson.datatype.jts
...
return serializer.writeValueAsBytes(avroConvertedObject); // => JsonMappingException
I also tried many configurations using: serializer.configure(...) but still failed.
Versions: Java 1.8, jackson-datatype-jts 2.3,
jackson-core 2.6.5, jackson-databind 2.6.5, jackson-annotations 2.6.5
Any suggestions?
Thanks!
If the SCHEMA member is really the case (we don't see the full error message), then you can switch it off. I use a mixin to do it, like this:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.avro.Schema;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
public class AvroGenerTests
{
abstract class IgnoreSchemaProperty
{
// You have to use the correct package for JsonIgnore,
// fasterxml or codehaus
#JsonIgnore abstract void getSchema();
}
#Test
public void writeJson() throws IOException {
BookAvro b = BookAvro.newBuilder()
.setTitle("Wilk stepowy")
.setAuthor("Herman Hesse")
.build();
ObjectMapper om = new ObjectMapper();
om.enable(SerializationFeature.INDENT_OUTPUT);
om.addMixIn(BookAvro.class, IgnoreSchemaProperty.class);
om.writeValue(new File("plik_z_gen.json"), b);
}
}
My req'ts got changed on me and I was told I needed to convert Avro objects straight to JSON without preserving any of the meta-data. My other answer herein that specified a method convertToJsonString converts the entire Avro object to JSON so that using a de-encoder you can re-create the original Avro object as an Avro object. That isn't what my mgt. wanted anymore so I was back to the old drawing board.
As a Hail Mary pass I tried using Gson and it works to do what I now had to do. It's very simple:
Gson gson = new Gson();
String theJsonString = gson.toJson(object_ur_converting);
And you're done.
2022 Avro field names
abstract class IgnoreSchemaPropertyConfig {
// You have to use the correct package for JsonIgnore,
// fasterxml or codehaus
#JsonIgnore
abstract void getClassSchema();
#JsonIgnore
abstract void getSpecificData();
#JsonIgnore
abstract void get();
#JsonIgnore
abstract void getSchema();
}
After finding the code example at https://www.programcreek.com/java-api-examples/?api=org.apache.avro.io.JsonEncoder I wrote a method that should convert any given Avro object (they extend GenericRecord) to a Json String. Code:
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.EncoderFactory;
import org.apache.avro.io.JsonEncoder;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
// ... Class header etc. ...
public static <T extends GenericRecord> String convertToJsonString(T event) throws IOException {
String jsonstring = "";
try {
DatumWriter<T> writer = new GenericDatumWriter<T>(event.getSchema());
OutputStream out = new ByteArrayOutputStream();
JsonEncoder encoder = EncoderFactory.get().jsonEncoder(event.getSchema(), out);
writer.write(event, encoder);
encoder.flush();
jsonstring = out.toString();
} catch (IOException e) {
log.error("IOException occurred.", e);
throw e;
}
return jsonstring;
}
The previous post answers the question correctly. I am just adding on to the previous answer. Instead of writing it to a file I converted it to a string before sending it as body in a POST request.
public class AvroGenerateJSON
{
abstract class IgnoreSchemaProperty
{
// You have to use the correct package for JsonIgnore,
// fasterxml or codehaus
#JsonIgnore abstract void getSchema();
}
public String convertToJson() throws IOException {
BookAvro b = BookAvro.newBuilder()
.setTitle("Wilk stepowy")
.setAuthor("Herman Hesse")
.build();
ObjectMapper om = new ObjectMapper();
om.enable(SerializationFeature.INDENT_OUTPUT);
om.addMixIn(BookAvro.class, IgnoreSchemaProperty.class);
String jsonString = om.writeValueAsString(b);
return jsonString;
}
}
Agree with Shivansh's answer. To add, there might be instances where we need to use the avro-generated pojo in other classes. Under the hood, spring uses jackson library in handling this so we need to override global jackson config by adding a class
#Configuration
public class JacksonConfiguration {
public abstract IgnoreSchemaProperty {
#JsonIgnore abstract void getSchema();
}
#Bean
public ObjectMapper objectMapper() {
ObjectMapper om = new ObjectMapper();
om.addMixIn(SpecificRecordBase.class, IgnoreSchemaProperty.class);
return om;
}
}
SpecificRecordBase -if we want to ignore the schema field from all avro generated classes. In this way, we can serialize/deserialize our avro classes and use it anywhere in our application without getting the issue.

Categories

Resources