Building a microservice in java using -
spring-boot version 2.2.6.RELEASE
graphql-spring-boot-starter version 5.0.2
Trying to persist record in MongoDB using graphql mutation, I successfully persisted through Single Object like below -
type ParentElement {
id: ID!
type: String
child: String
}
But when trying out with nested object, I am seeing following error -
Caused by: com.coxautodev.graphql.tools.SchemaError: Expected type 'ChildElement' to be a GraphQLInputType, but it wasn't! Was a type only permitted for object types incorrectly used as an input type, or vice-versa?
My Schema is as follows -
schema {
query: Query
mutation: Mutation
}
type ChildElement {
make: String
model: String
}
type ParentElement {
id: ID!
type: String
child: ChildElement
}
type Query {
findAllElements: [ParentElement]
}
type Mutation {
createElement(id: String, type: String, child: ChildElement): ParentElement
}
Pojo Classes & Mutation are as follows -
#Document(collection="custom_element")
public class ParentElement {
private String id;
private String type;
private ChildElement child;
}
public class ChildElement {
private String make;
private String model;
}
#Component
public class ElementMutation implements GraphQLMutationResolver {
private ElementRepository elementRepository;
public ElementMutation(ElementRepository elementRepository) {
this.elementRepository = elementRepository;
}
public ParentElement createElement(String id, String type, ChildElement child) {
ParentElement element = new ParentElement()
elementRepository.save(element);
return element;
}
}
#Component
public class ElementQuery implements GraphQLQueryResolver {
private ElementRepository elementRepository;
#Autowired
public ElementQuery(ElementRepository elementRepository) {
this.elementRepository = elementRepository;
}
public Iterable<ParentElement> findAllElements() {
return elementRepository.findAll();
}
}
#Repository
public interface ElementRepository extends MongoRepository<ParentElement, String>{
}
I want to save following json representation in mongo db -
{
"id": "custom_id",
"type": "custom_type",
"child": {
"make": "custom_make",
"model": "Toyota V6",
}
}
I tried several things but when starting server always getting same exception. The above json is a simple representation. I want to save much more complex one, the difference with other examples available online is that I don't want to create separate mongo object for child element as shown with Book-Author example available online.
Graphql type and input are two different things. You cannot use a type as a mutation input. That is exactly what the exception is about: Expected type 'ChildElement' to be a GraphQLInputType, but it wasn't, the library was expecting an input but found something else (an object type).
To solve that problem, create a child input:
input ChildInput {
make: String
model: String
}
type Mutation {
createElement(id: String, type: String, child: ChildInput): ParentElement
}
You can also have a look at this question: Can you make a graphql type both an input and output type?
Related
I have a simple hierarchy in neo4j directly derived from the business model.
#Node
public class Team {
#Id private String teamId;
private String name;
}
#Node
public class Driver {
#Id private String driverId;
private String name;
#Relationship(direction = Relationship.Direction.OUTGOING)
private Team team;
}
#Node
public class Car {
#Id private String carId;
private String name;
#Relationship(direction = Relationship.Direction.OUTGOING)
private Driver driver;
}
which results in the corresponding graph (Team)<--(Driver)<--(Car) usually all requests start at Car.
A new use case needs to create a tree structure starting at Team nodes. The Cypher query aggregates the data on neo and returns it to SDN.
public List<Projection> loadHierarchy() {
return neo4jClient.query("""
MATCH(t:Team)<--(d:Driver)<--(c:Car)
WITH t, d, collect(distinct c{.carId, .name}) AS carsEachDriver
WITH t, collect({driver: d{.driverId, .name}, cars: carsEachDriver }) AS driverEachTeam
WITH collect({team: t{.teamId, .name}, drivers: driverEachTeam }) as teams
RETURN teams
""")
.fetchAs(Projection.class)
.mappedBy((typeSystem, record) -> new Projection() {
#Override
public Team getTeam() {
return record.get... // how to access single object?
}
#Override
public List<Retailers> getRetailers() {
return record.get... // how to access nested list objects?
}
})
.all();
}
The result is a list of following objects:
{
"drivers": [
{
"driver": {
"name": "Mike",
"driverId": "15273c10"
},
"cars": [
{
"carId": "f4ca4581",
"name": "green car"
},
{
"carId": "11f3bcae",
"name": "red car"
}
]
}
],
"team": {
"teamId": "4586b33f",
"name": "Blue Racing Team"
}
}
The problem is now, how to map the response into an according Java model. I don't use the entity classes.
I tried multi-level projection with nested interfaces.
public interface Projection {
Team getTeam();
List<Drivers> getDrivers();
interface Drivers {
Driver getDriver();
List<Cars> getCars();
}
interface Driver {
String getDriverId();
String getName();
}
interface Car {
String getCarId();
String getName();
}
interface Team {
String getTeamId();
String getName();
}
}
I struggle to access the nested lists and objects, to put them into the model.
SDN is the Spring Boot Starter in version 2.6.3.
An example how to map a nested object in a list would be a good starting point.
Or may be my approach is totally wrong? Any help is appreciated.
Projection are not meant to be something like a view or wrapper of arbitrary data.
Within the context you can get a Neo4jMappingContext instance.
You can use this to obtain the mapping function for an already existing entity.
With this, you do not have to take care about mapping the Car and (partially because of the team relationship) the Drivers.
BiFunction<TypeSystem, MapAccessor, Car> mappingFunction = neo4jMappingContext.getRequiredMappingFunctionFor(Car.class);
The mapping function accepts an object of type MapAccessor.
This is a Neo4j Java driver type that is implemented besides others also by Node and MapValue.
You can use those values from your result e.g. drivers in a loop (should be possible to call asList on the record) and within this loop you would also assign the cars.
Of course using the mapping function would only make sense if you have a lot more properties to map because nothing in the return structure (as you already said between the lines) applies to the entity structure regarding the relationships.
Here is an example of using the mapping function and direct mapping.
You have to decide what matches best for your use case.
public Collection<Projection> loadHierarchy() {
var teamMappingFunction = mappingContext.getRequiredMappingFunctionFor(Team.class);
var driverMappingFunction = mappingContext.getRequiredMappingFunctionFor(Driver.class);
return neo4jClient.query("""
MATCH(t:Team)<--(d:Driver)<--(c:Car)
WITH t, d, collect(distinct c{.carId, .name}) AS carsEachDriver
WITH t, collect({driver: d{.driverId, .name}, cars: carsEachDriver }) AS driverEachTeam
WITH {team: t{.teamId, .name}, drivers: driverEachTeam } as team
RETURN team
""")
.fetchAs(Projection.class)
.mappedBy((typeSystem, record) -> {
Team team = teamMappingFunction.apply(typeSystem, record.get("team"));
List<DriverWithCars> drivers = record.get("team").get("drivers").asList(value -> {
var driver = driverMappingFunction.apply(typeSystem, value);
var cars = value.get("carsEachDriver").asList(carValue -> {
return new Car(value.get("name").asString());
});
return new DriverWithCars(driver, cars); // create wrapper object incl. cars
});
return new Projection(team, drivers);
})
.all();
}
(Disclaimer: I did not execute this on a data set, so there might be typos or wrong access to the record)
Please note that I changed the cypher statement a little bit to have get one Team per record instead of the whole list.
Maybe this is already what you have asked for.
I have a java class representing a JSON using Jackson. All of the fields, with one exception, can be translated using no annotations at all. 1-to-1, simple translations (although some of them are nested POJOs).
#Data
#AllArgsConstructor
#NoArgsConstructor
public class MyPojo {
private String someString;
private AnotherPojo someOtherPojo;
//The problem child:
private Object value;
}
The field value which is an exception to this rule, can represent any JSON field matching value* where * is a wildcard of indefinite length. That means valueString or valueReference in JSON will be assigned to this field with the assertion that only one may be present.
{
"someString": "asdasdadasdsa",
"someOtherPojo": {
"someOtherProperty": "whatever"
},
"valueCodeableConcept": {
"text": "text value",
"coding": [
{
"code": "my-code"
}
]
}
}
Using a custom deserializer on the top-level class, I can scrape all of the fields from the root node (baseNode in the following example) that start with value and set the value field appropriately. That works great! However, in doing so, I now have to set every other field in this MyPojo class manually in my deserializer, and I have to put a custom copy of this deserializer on each POJO that uses a field like value*.
private Object parseValueX(JsonNode baseNode, DeserializationContext context) throws IOException {
//Find the concrete implementation referred to by the value[x] field
Set<String> concreteNames = new HashSet<>();
baseNode.fieldNames().forEachRemaining(name -> {
if (name.startsWith("value")) {
concreteNames.add(name);
}});
if (concreteNames.isEmpty()) {
return null;
}
if (concreteNames.size() > 1) {
throw JsonMappingException.from(context, "The field value[x] must have no more than one concrete " +
"implementation, ex: valueCode, valueCodeableConcept, valueReference");
}
String concreteName = concreteNames.stream().findFirst().orElseThrow(() -> new RuntimeException(""));
JsonNode jsonSource = baseNode.get(concreteName);
//...deserialize from jsonSource, solved, but not relevant to question...
}
To make this apply to any value* property on any POJO, I tried to move the deserializer to the value attribute in the POJO (whereas it's on the top-level resource now). The first flaw is that the deserializer isn't even invoked unless the JSON property exactly matches value. What I actually need is for the entire parent JSON resource to be passed to that field-specific deserializer, so that I may find the matching field and assign it -- OR -- I need to be able to have the deserializer on MyPojo only assign the one field value and allow the automatic deserialization to take care of the others. How do I do either of these?
For those curious about my motivation, I am implementing the HL7 FHIR Specification, which specifies generic attributes called value[x] (here's one example: https://www.hl7.org/fhir/extensibility.html#Extension) where [x] becomes the type of the resource.
I think a good fit for you problem is #JsonAnySetter. This method annotation tells Jackson to route unknown properties to it. the arg (in your case) is a Map containing the json tree of the unknown property. if I understand your code properly, the name of the value property contains the class name of the target Pojo. so once you have a class name, you can tell Jackson how to "deserialize" the map into an instance of the target class.
Here is an example based on the code from the question
public class MyPojo {
public String someString; // made properties into public for this example...
public AnotherPojo someOtherPojo;
public Object value;
#JsonAnySetter
public void setValue(String name, Object value) {
System.out.println(name + " " + value.getClass());
System.out.println(value);
// basic validation
if (name.startsWith("value") && value instanceof Map) {
String className = "com.company." + name.substring("value".length());
System.out.println(name + " " + value.getClass() + " " + className);
System.out.println(value);
try {
// nice of Jackson to be able to deserialize Map into Pojo :)
ObjectMapper mapper = new ObjectMapper();
this.value = mapper.convertValue(value, Class.forName(className));
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(this.value + " " + this.value.getClass());
}
}
}
public class AnotherPojo {
public String someOtherProperty;
}
public class CodeableConcept {
public String text;
public Code[] coding;
}
public class Code {
public String code;
}
Is it possible to dynamically create a GraphQL schema ?
We store the data in mongoDB and there is a possibility of new fields getting added. We do not want any code change to happen for this newly added field in the mongoDB document.
Is there any way we can generate the schema dynamically ?
Schema is defined in code, but for java(schema as pojo), when new
attribute is added, you have to update and recompile code, then
archive and deploy the jar again. Any way to generate schema by the
data instead of pre-define it?
Currently we are using java related projects (graphql-java, graphql-java-annotations) for GraphQL development.
You could use graphql-spqr, it allows you auto-generate a schema based on your service classes. In your case, it would look like this:
public class Pojo {
private Long id;
private String name;
// whatever Ext is, any (complex) object would work fine
private List<Ext> exts;
}
public class Ext {
public String something;
public String somethingElse;
}
Presumably, you have a service class containing your business logic:
public class PojoService {
//this could also return List<Pojo> or whatever is applicable
#GraphQLQuery(name = "pojo")
public Pojo getPojo() {...}
}
To expose this service, you'd just do the following:
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withOperationsFromSingleton(new PojoService())
.generate();
You could then fire a query such as:
query test {
pojo {
id
name
exts {
something
somethingElse
} } }
No need for strange wrappers or custom code of any kind, nor sacrificing type safety. Works with generics, dependency injection, or any other jazz you may have in your project.
Full disclosure: I'm the author of graphql-spqr.
After some days' investigation. I found it is hard to generate schema dynamically in Java (or cost is so high).
Well, from another way. I think we can use Map as a compromised way to accomplish that.
POJO/Entity
public class POJO{
#GraphQLField
private Long id;
#GraphQLField
private String name;
// ...
#GraphQLField
private GMap exts;
}
GMap is a customized Map (Because Map/HashMap is a JDK inner class which could not make as GraphQL Schema but only extend).
GMap
public class GMap extends HashMap<String, String> {
#GraphQLField
public String get(#GraphQLName("key") String key) {
return super.get(key);
}
}
Retrieve data from Client
// query script
query test
{
your_method
{
id
name
exts {
get(key: "ext") // Add a extended attribute someday
}
}
}
// result
{
"errors":[],
"data":
{
"list":
[
{"id":1, name: "name1", exts: {"get": "ext1"}},
{"id":2, name: "name2", exts: {"get": "ext2"}}
]
}
}
My resource is
#GET
#Path("/items")
public MyCollection<Items> getItems()throws Exception{
//Code to return MyCollection<items>
}
My Item class is
#XmlRootElement
public class Item{
private int id;
private String name;
//Have getters and Setters.
}
And My collection class is Generic as below.
public class MyCollection<T> extends MyBaseCollection{
private java.util.Collection<T> items;
private int count;
}
When i try to generate doc using enunciate. The sample Json has only the item and count and the fields of Item class is not getting reflected.
My sample Json generated is
{
"items" : [ {
}, {
}, {
}, {
}, {
}, {
}, {
}, {
} ],
"count" : ...,
}
How to get id,name inside the Item in the generated sample Json?
Thanks.
This is a limitation that i have run into as well, there is no way to specify #TypeHint on a nested object. To support documentation, consider creating a custom collection that defines "items" as a collection of specific class instead of generic.
If you have an idea of how you would want this to work (using the generic type) I suggest submitting enhancement request to Enunciate team.
I have a similar problem where I am returning a Map and I can't #TypeHint this.
I was given json file and third party class:Dealer and interface IDealerAttributes (I can not change either of them);
(I remove package name and imports to make the code simple)
JSON file
{
"serviceURL": "com.mycompany.serviceURL",
"dealerAttributes": [
{
"language": "language0",
"dealerAttributeName": "dealerAttributeName0",
"updateDate": 0
},
{
"language": "language1",
"dealerAttributeName": "dealerAttributeName1",
"updateDate": 1
}
]
}
class Dealer {
private String serviceURL;
private List dealerAttributes;
public Dealer() {
dealerAttributes = new ArrayList();
}
//Getters and Setters...
}
public interface IDealerAttributes {
public String getLanguage();
public String getDealerAttributeName();
public long getUpdateDate();
}
once I use:
gson.fromJson(jsonObj.toString(), Dealer.class);
I will get exception from this line:
Exception unmarshalling json String into Object: com.google.gson.JsonParseException: The JsonDeserializer com.google.gson.DefaultTypeAdapters$CollectionTypeAdapter#60e26ffd failed to deserialize json object [{"language":"language0","dealerAttributeName":"dealerAttributeName0","updateDate":0},{"language":"language1","dealerAttributeName":"dealerAttributeName1","updateDate":1}] given the type java.util.List
How can I read this json file based on Dealer.class, IDealerAttributes?
But I can add one class, let's say:
public class DealerAttributes implements IDealerAttributes {
private String language;
private String dealerAttributeName;
private long updateDate;
public DealerAttributes() {
}
//Getters and Setters...
}
Since I am new to json/gson, would you please give detailed instruction to help me out? Thanks.
[added]
Consider if there are 100 fields in Dealer class, and there are another 100 interface used/nested in Dealer. I am thinking whether anyone have experience using this way: (MyType is interface)
gson.registerTypeAdapter(MyType.class, new MyType());`
You could map it to a List of Maps and then use a BeanMapper like http://code.google.com/p/orika/ to get some more informative error messages