Graphql: `defaultValue` makes property optional and it can be overridden by explicit `null`

Created on 27 Apr 2021  路  11Comments  路  Source: nestjs/graphql

I'm submitting a...


[ ] Regression 
[x] Bug report
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

I am using the code first approach.

TLDR: in the @Field() decorator, the defaultValue option always makes the property nullable (regardless of the nullable option), even though null may not be an acceptable value in an input type.

The @Field() decorator can take an options object to configure the GraphQL property to be nullable or not and to have a default value or not.

Illustration:

@InputType()
export class CreateUserInput {
  @Field((type) => [String], { nullable: false, defaultValue: [] }) // nullable: false makes no difference at all
  details: string[];
}

The previous code will generate the following schema.

input CreateUserInput {
  details: [String!] = []
}

This means that the values in the details array must be non-null strings, that details has a default value of an empty array BUT details itself can be set to null.

This means that ALL the following mutations are valid but the last one should not. details should either be an array or not be present at all.

mutation CreateUser {
  explicit: createUser(input: { details: ["Nest", "is", "so", "cool"] })
  omitted: createUser(input: {})
  explicitly_null: createUser(input: { details: null })
}

Expected behavior

I should be able to define input fields with a default value AND make them required to prevent them from accepting the value null as user input.

The typescript code above should produce the following schema (notice the TWO exclamation marks). Thus, the property does not accept null as an explicit value and uses the default value.

input CreateUserInput {
  details: [String!]! = []
}

Minimal reproduction of the problem with instructions

https://github.com/Haltarys/Nest-GraphQL-Bug

What is the motivation / use case for changing the behavior?

I expect the details parameter to always be an array, regardless of the user input.

Environment


Nest version: 7.6.13

For Tooling issues:
- Node version: 15  
- Platform: Ubuntu 20.04  

Others:

All 11 comments

Note: I used an array in an input type to illustrate the problem but it works with any type as long as it is an argument of a query/mutation.

type Query {
   getProducts(sortAsc: Boolean = true): [Product!]! # sortAsc can be null
}

# Any of these values can be overridden with null
input UserInput {
   age: Int = 42
   isAdmin: Boolean = false
   name: String = "Bruce Wayne"
}

I should be able to define input fields with a default value AND make them required to prevent them from accepting the value null as user input.

This isn't possible. If you define a default value, then the logic is, you are definitely expecting a null value to come through, thus assigning the default value. Or, put another way, if a field is non-nullable, a default value is useless. You are requiring a value to be given, so the client must pass a value other than null.

The issue seems to be that defaultValue for the array type is always ignored. No matter what is put in defaultValue, if null is passed by the client, null goes through and the defaultValue isn't set.

Scott

@smolinari What I mean by "make them required" is that they cannot accept null. But of course, if they have a default value, then the field is not required per se. But that doesn't mean that it should be nullable.

When working with just express and apollo-server, this schema is perfectly valid.

input A {
   age: Int! = 42 # What I want to do
}

input B {
   age: Int = 42 # The current behavior
}

In A, age is an Int with a default value of 42. But if I decide to override it, I cannot override it with null.
But in B, I can set age to null which isn't a valid value for my usecase.

That's what I am trying to show here:
```gql
mutation CreateUser {
explicit: createUser(input: { details: ["Nest", "is", "so", "cool"] }) # Overriding with correct values
omitted: createUser(input: {}) # Omitting, thus the default value [] is used
explicitly_null: createUser(input: { details: null }) # Overriding with null, which is what I want to prevent
}

@Haltarys - I see what you are getting at. And my comment above is also incorrect. "null" is a value in the end.

My understanding is, as soon as a field is non-nullable, default values are useless.

input A {
   age: Int! = 42  # 42 will never be set 

In the same vein:

input CreateUserInput {
  details: [String!]! = []  # the empty array '[]' will never be set, as details must have an input value (which also can't be null).  
}

So, from that logic, you can't have a default value AND not override with null. Or put another way, the non-null field means it is mandatory, thus, default values with non-null fields are useless.

So, reiterating my understanding:

  • Nullable fields can be empty, contain null and default values will work, if null isn't sent (so overridable with null).
  • Non-nullable fields are mandatory (i.e. can't be empty) and can't have null as a value. Default values have no effect.

That's it. There is no way to have a non-nullable field and a default value and it makes no sense to want this, because of rule 2 above.

Scott

@smolinari That is the problem. age: Int! = 42 IS set.

Non-nullable fields are mandatory (i.e. can't be empty) and can't have null as a value. Default values have no effect.

They do have an effect. Non-nullable fields are not mandatory if they have a default value. They're just non-nullable.

To prove it, I have made a repo with just apollo-server and graphql. Here is the link. Make sure you are on the branch apollo-graphql. There is an example file named example.gql with the queries. You'll see that the one named explicit_null_A is not valid.

I see what you mean, but wanting a non-nullable field with a default still makes no sense to me. The spec even says, non-null fields are required and shouldn't accept ommissions. I'm wondering if this is a bug in Apollo Server???

All that aside, what is the use case where you need a default value on a required field or rather a default value that can't be null overridden?

Scott

The way I see it, being nullable and having a default value are two unrelated metadata (I'm not sure it's the right word but you get it) about a field.
They both allow said field to be omitted: in the first case, the value will be null because nothing was provided, and in the second, it will fall back to the default value.

a default value that can't be null overridden?

That's exactly what I'm trying to do. And the syntax age: Int! = 42 allows exactly that: age can be omitted, and overridden, but not with null. The fact that a field is required or not is tied to my application's logic, not GraphQL. I apologize if my explanation was unclear.

Also, the spec says that all types are nullable by default but @nestjs/graphql makes them non-nullable by default. So it wouldn't be the first time we don't stick perfectly to the spec. (And I think it's better with Nest's way). But I'm rambling.

The spec even says, non-null fields are required and shouldn't accept ommissions. I'm wondering if this is a bug in Apollo Server???

OK. Just to prove the point, I made another GraphQL server using express-graphql. This is based on the official GraphQL tutorial. If that's not spec-compliant, I don't know what is. ;-)
The result is the same. Find it here. Make sure you are on branch express-graphql.

We see the same issue. In Apollo Server, you can have a non-null field with a default value.

The ! only means that the field cannot be null. If a default value is set, the ! does not imply that you must provide an argument in Apollo Server.

The resulting behavior in NestJS is incorrect:

@Args('id', { defaultValue: '42',  nullable: false }) // note: nullable: false ignored by NestJS

If the consumer passes in null for this argument, then the resolver gets null as its value. Not the defaultValue! The field is nullable because NestJS ignored nullable: false. null is a value, so therefore the defaultValue doesn't get applied.

NestJS should be generating this GQL, which would work correctly:

(id: String! = "42")

This is a blocker to us on a large internal project.

I think even the GraphQL spec has been changed (or I seriously overlooked it).

In addition to not accepting the value null, it also does not accept omission. For the sake of simplicity nullable types are always optional and non鈥恘ull types are always required.

http://spec.graphql.org/June2018/#sec-Type-System.Non-Null

Which clearly defines "null" as not acceptable to a non-null field in an input object or arg.

Scott

As I mentioned in my previous comment, using an officially endorsed package by GraphQL (therefore spec compliant), the expected behaviour can be achieved. So there is definitely a contradiction here, but it'd be nice to be able to use that trick. That would minimise manual input checking.

Was this page helpful?
0 / 5 - 0 ratings