graphql-spqr: @GraphQLNonNull ignored on Input objects when @JsonProperty is used

Hello,

recently the frontend team informed me that on some of our inputs there are no required fields. I’ve traced down the bug to Jackson’s @JsonProperty which was added some time ago on constructor parameters (constructor itself was annotated with @JsonCreator).

Here is the MVCE that produces no ! in generated GQL schema (i.e. every field is nullable):

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import graphql.schema.idl.SchemaPrinter;
import io.leangen.graphql.GraphQLSchemaGenerator;
import io.leangen.graphql.annotations.GraphQLArgument;
import io.leangen.graphql.annotations.GraphQLNonNull;
import io.leangen.graphql.annotations.GraphQLQuery;

class Test {
    public static void main(String[] args) {
        GraphQLSchemaGenerator schemaGenerator = new GraphQLSchemaGenerator()
                .withOperationsFromSingleton(new Test());
        System.out.println(new SchemaPrinter().print(schemaGenerator.generate()));
    }
    @GraphQLQuery(name = "RegisterUser")
    public String registerUser(@GraphQLArgument(name = "input") @GraphQLNonNull UserInput input) {
        return input.getFirstName();
    }
}

class UserInput {
    private String firstName;
    private String lastName;

    @JsonCreator
    public UserInput(@JsonProperty("firstName") String firstName,
                     @JsonProperty("lastName") String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @GraphQLNonNull
    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

Schema produced:

#Query root
type Query {
  RegisterUser(input: UserInputInput!): String @_mappedOperation(operation : "__internal__")
}

input UserInputInput @_mappedType(type : "__internal__") {
  firstName: String
  lastName: String
}

Parameter input is okay, but firstName in UserInputInput is nullable (not required), and there is @GraphQLNonNull annotation on getter getFirstName.

I’ve tried following without success:

  • adding setters, and annotation it @GraphQLNonNull UserInput setFirstName (String name)
  • adding setter, but annotating parameter UserInput setFirstName (@GraphQLNonNull String name)

Correct schema is produced when @JsonProperty is removed, e.g. with this constructor:

    public UserInput(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

(It seems that @JsonCreator annotation does not matter)

input UserInputInput @_mappedType(type : "__internal__") {
  firstName: String!
  lastName: String
}

So, I thought that @JsonProperty is considered as “truth source” because it has JsonProperty.required field which is by default false. But, adding @JsonProperty(value = "firstName", required = true) on constructor parameter (or on private field itself, or both) does not change anything.

I’m using graphql-spqr:0.9.9 and graphql-java:11.0.

Is this a bug or is there any workaround for this? Maybe using @GraphQLInputField? (I haven’t used that before).

P.S. Another question, I saw on gitter that someone asked how to get rid of @_mappedOperation and @_mappedType, but answer was vague. Is it possible now, did you find out a way?

Thank you in advance for answer and for this very helpful library!

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 15 (5 by maintainers)

Most upvoted comments

@EnilPajic I’ve opened a new issue for JsonProperty#required: https://github.com/leangen/graphql-spqr/issues/287

Hey everyone. Sorry for the silence on my part, I’m away on a business trip these days.

As for the reason the annotation is apparently ignored… When inputs are mapped using Jackson, the logic is as follows:

  • Find the explicitly @GraphQLInputField annotated element
  • Find the primary mutator that Jackson will use to deserialize the values
  • Find the field belonging to the property
  • Merge the types of all 3

So for the case such as:

@JsonCreator
public UserInput(@JsonProperty("firstName") String firstName,
                 @JsonProperty("lastName") String lastName) {
    this.firstName = firstName;
    this.lastName = lastName;
}

@GraphQLNonNull
public String getFirstName() {
    return firstName;
}

There’s no explicitly annotated element, and the constructor is the primary mutator (that’s what will be used to deserialize), so only the field type and the ctor parameter type get merged (getter is not involved), thus the annotation is ignored. (So @Kirintale pretty much nailed it)

To force the logic to take the getter into account, you can mark it with @GraphQLInputField. Or move @GraphQLNonNull to the ctor param or the field.

The reason it works the way it does is to avoid the situation where a bunch of unrelated language elements have to be reasoned about at the same time to know what the mapping will be. The field is always taken into account to allow the usage of things like Lombok. The primary mutator is taken into account because that is what will actually be used by Jackson. The only remaining element is the one the user explicitly decides to involve.

P.S. Another question, I saw on gitter that someone asked how to get rid of @_mappedOperation and @_mappedType, but answer was vague. Is it possible now, did you find out a way?

SPQR uses those to provide some its functionality, so you can’t get rid of them completely without tricky repercussions… If you meant how to avoid printing them, there were changes in the SchemaPrinter that lets you skip directives (includeDirectives), but that’s coming from graphql-java so I have no control over it. If you need a more granular choice if what to print, I’d suggest making a PR on graphql-java.

I’ll read thought the rest of the thread as soon as I get a chance and respond to whatever else there is…

Hi!

Thank you for testing this out. Your understanding of @JsonCreator is right, if present, setters won’t be called.

But I’ve noticed something: if @JsonCreator is removed, the incorrect schema is produced again, e.g. with this code:

class UserInput {
    private String firstName;
    private String lastName;

    //no JsonCreator
    public UserInput(@JsonProperty("firstName") String firstName,
                     @JsonProperty("lastName") String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    
    @GraphQLNonNull
    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

So, in this case, it seems that @JsonCreator does not matter (as stated in my original post). But what matters is the @JsonProperty declaration on constructor parameters. If I remove them, everything works fine (with @GraphQLNonNull on getter):

class UserInput {
    private String firstName;
    private String lastName;


    //no matter if @JsonCreator is present or not, schema is correct
    public UserInput(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @GraphQLNonNull
    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

The above code indeed produces correct schema, so I assume that @JsonProperty is the reason? And if @GraphQLNonNull is working without @JsonProperty but not with it, this may be a bug?