Comments (8)
Thanks for the report!
graphql-java drastically changed how it handles default values and a lot of logic in SPQR had to be reimplemented. Somewhere in that migration your case fell through the cracks. I'll investigate what can be done.
from graphql-spqr.
Thank you for your quick reply! It would be really appreciated :)
from graphql-spqr.
Oof, this is tricky business... Here's the breakdown.
In earlier versions, graphql-java permitted any Java object as the default value, and it would then do its best to represent such value in the serialized schema. And what SPQR did was simply deserialize the provided default value string into an object of the corresponding class and had it over to graphql-java. In your case, {}
would deserialize to a Pagination
instance, which would then get serialized back into schema as {limit : 10, offset : 0}
.
Then, recent versions of graphql-java got rather strict and started only accepting values that can be unambiguously represented in the schema - in Java terms that would mean primitives, strings, lists and maps. No arbitrary Java objects allowed. So the way SPQR adapted was to deserialize the given default value to just the simple types mentioned previously. In your example, it means {}
gets deserialized to an empty map, with gets serialized back into the schema as nothing.
At runtime, in both cases, you receive an equal instance of Pagination
. This is because in the first case the default value is already a Pagination
that simply gets passed, and in the second the default value is a map that gets converted into Pagination
at each invocation. So the there should be no observable differences at runtime. But the printed schema is, as you've noticed, different.
I am not exactly sure yet how to approach this. It sounds possible to emulate the earlier behavior of graphql-java by deserializing the provided default value string to an instance of the corresponding class just as before (thus triggering the appropriate constructor and populating the fields), but then convert it into a map before handing it back to graphql-java. In your example, that would mean deserializing {}
to Pagination
first, just as before, and then converting that instance into a map {limit : 10, offset : 0}
. This also sounds more correct, as the default values will get captured as they really are in Java. But I have to investigate a bit more to see whether this is indeed possible and will not have any unwanted effects.
from graphql-spqr.
This also sounds more correct, as the default values will get captured as they really are in Java. But I have to investigate a bit more to see whether this is indeed possible and will not have any unwanted effects.
On second though, this really isn't true, is it? After all, you're not surprised that you're seeing
input PaginationInput {
limit: Int
offset: Int
}
and not
input PaginationInput {
limit: Int = 10
offset: Int = 0
}
despite 10 and 0 being the default for the corresponding Java fields, right? To achieve the latter, you'd have to do something like:
public class Pagination {
@GraphQLInputField(defaultValue = "10")
public Integer limit = 10;
@GraphQLInputField(defaultValue = "0")
public Integer ffs = 0;
}
So why would you then be surprised to see
PaginationInput = {}
and not
PaginationInput = {limit : 10, offset : 0}
when {}
is what was specified and not {limit : 10, offset : 0}
? The above 2 cases are semantically equivalent, right? In both cases the GraphQL values and the Java values differ, and have to be brought in line explicitly.
Hmmm... I'm really unsure what to make of this yet 🫤
from graphql-spqr.
I think the current behavior is consistent, so I'm inclined to leave it as-is. But, here's how you can pretty easily get the behavior you're after by yourself:
public class JsonReserializer implements DefaultValueProvider {
private static final AnnotatedType OBJECT = TypeFactory.annotatedClass(Object.class, new Annotation[0]);
private final ValueMapper valueMapper;
public JsonReserializer() {
this.valueMapper = Defaults.valueMapperFactory().getValueMapper();
}
@Override
public DefaultValue getDefaultValue(AnnotatedElement targetElement, AnnotatedType type, DefaultValue initialValue) {
// This is just guarding against exotic edge-cases, it's not strictly necessary
if (initialValue.getValue() == null || initialValue.getValue().getClass() != String.class || type.getType() == String.class) {
return initialValue;
}
return initialValue
.map(v -> valueMapper.fromString((String) v, type)) //String -> Pagination
.map(v -> valueMapper.fromInput(v, OBJECT)); //Pagination -> Map (as graphql-java wants it)
}
}
You can then either register this as the default provider, or use it where applicable only:
@GraphQLQuery(name = "items")
public CompletableFuture<List<RelayTest.Book>> items(
@GraphQLArgument(
name = "pagination",
description = "Pagination of results.",
defaultValue = "{}",
defaultValueProvider = JsonReserializer.class)
final Pagination pagination) {
return ...;
}
Note: Make sure Pagination
has fields visible for serialization (e.g. make them public or have public getters). Previously Pagination
instances were only ever deserialized, now they're being serialized as well.
Do you find this solution acceptable?
from graphql-spqr.
Hi!
Thank you for all the context and advice. I didn't know about the way you could use @GraphQLInputField(defaultValue = "10")
in a class like that, that is really nice! And maybe one way I can go about defining the default values in the future.
I also tried out the JsonReserializer
solution which also worked great. That would make it possible to have the schema look the same as it used to and we could keep {}
as the default value, so that would also be an option.
The variables are public in the Pagination class, I just forgot to change it in my example, sorry about that. I am using Lombok and have the following annotations @Data @NoArgsConstructor @AllArgsConstructor @Builder
on the class.
However, the issue still remains :(
When I have defaultValue
set in the GraphQLArgument
in the java code, either as "{}"
or as
"{\"limit\":10, \"offset\":0}"
and I write my query like this with the separate fields defined:
query GetItems( $limit: Int, $offset: Int) {
items(
pagination: {limit: $limit, offset: $offset}
) {
name
color
}
}
With input:
{
"limit": 5,
"offset": 0
}
Then I still get the following error:
graphql.AssertException: Expected a value that can be converted to type 'Int' but it was a 'LinkedHashMap'
at graphql.Assert.assertNotNull(Assert.java:17)
at graphql.scalar.GraphqlIntCoercing.valueToLiteralImpl(GraphqlIntCoercing.java:94)
at graphql.scalar.GraphqlIntCoercing.valueToLiteral(GraphqlIntCoercing.java:140)
at graphql.execution.ValuesResolverConversion.externalValueToLiteralForScalar(ValuesResolverConversion.java:148)
at graphql.execution.ValuesResolverConversion.externalValueToLiteral(ValuesResolverConversion.java:132)
at graphql.execution.ValuesResolverConversion.valueToLiteralImpl(ValuesResolverConversion.java:77)
at graphql.execution.ValuesResolver.valueToLiteral(ValuesResolver.java:220)
at graphql.validation.rules.VariableTypesMatch.checkVariable(VariableTypesMatch.java:68)
at graphql.validation.RulesVisitor.lambda$checkVariable$12(RulesVisitor.java:154)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at graphql.validation.RulesVisitor.checkVariable(RulesVisitor.java:154)
at graphql.validation.RulesVisitor.enter(RulesVisitor.java:79)
Maybe the error is strictly related to how the default value gets interpreted and parsed when generating the schema, but it is more something that fails later in run time, since the other formats of the query that I posted in my initial question still works. Maybe this is outside of the graphql-spqr scope?
Another question just because I got curious. With the new default value validation changes made in graphql-java, does this mean it is no longer recommended to send input values as objects? (And I do agree that it was a bit too loose validation before, so having it stricter sounds great! We found a couple of miss-matching types when upgrading as well.)
from graphql-spqr.
I managed to replicate this without SPQR, so it really is a bug in graphql-java. I opened an issue there: graphql-java/graphql-java#3276
With the new default value validation changes made in graphql-java, does this mean it is no longer recommended to send input values as objects?
Nothing perceivable should have changed, and I don't see anything wrong with how you're using the variables. It's really just a bug that will likely get fixed quickly 🤞
I'll close this issue now as it seems the SPQR part has been addressed.
from graphql-spqr.
Thank you! I should have checked that before reporting it to you.
Looking forward to the new release with the fix!
from graphql-spqr.
Related Issues (20)
- Setup GitHub Pages for documentation
- Apply bean validation annotations only if an appropriate group is enabled [Breaking]
- How to use executionResult data HOT 2
- Failing tests - ComplexityTest class
- Explicitly register Java classes to the GraphQL schema. HOT 4
- How to map a java class to GraphQL interface without using @GraphQLInterface annotation? HOT 5
- Best way to error handling HOT 1
- Help in Handling Exceptions with SPQR HOT 1
- Question: Extending an InputObject type and handle it in SPQR HOT 3
- SDL Printing Fails on Schema Directives with Arguments HOT 4
- Spring 3.2: Cannot resolve parameter names for constructor public io.leangen.graphql.spqr.spring.web.dto.GraphQLRequest(java.lang.String,java.lang.String,java.lang.String,java.util.Map) HOT 2
- Compile parameters to support Spring Boot 3.2.1 (Spring Framework 6.1) HOT 1
- null values sent in response json HOT 1
- Schema Generation Regression in 0.12.4 HOT 1
- Polymorphism type on input
- Scalars containing an Instant cannot be serialized over subscription
- Schema generation at compile time?
- BUG: The DelegatingTypeResolver breaks when updating the GraphQLSchema using a Visitor HOT 1
- Feature Request: Add support for GraphQLOutputType to the SchemaTransformer HOT 39
- Make a public OperationMapper#createResolver overload to help with creating custom field resolvers
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from graphql-spqr.