Repository: graphql-rust/juniper (language: rust)

Issue:
Input value defaulting behaviour is wrong
**Describe the bug**
The current behaviour for default values does not align with the GraphQL spec, or the reference implementation:

1. For a non-null field with a default value, it should be possible to override the value with a null value:
```graphql
# Schema
type Query {
  test(arg: Int = 100): Int
}
# Query
query {
  test(arg: null)
}
```
Currently Juniper takes the default value in this case.

2. A similar issue applies for variables:
```graphql
query ($arg: Int) {
  test(arg: $arg)
}
// or even with a variable default
query ($arg: Int = 10) {
  test(arg: $arg)
}
```
Passing an explicit `null` as the value for `$arg` should pass through to the `test` resolver.

3. When declaring a non-optional field as having a default value, Juniper currently downgrades the type to non-null when adding the default. This incorrectly implies that an explicit null is allowable.

E.g.:
```rust
#[graphql_object]
impl Query {
    fn non_null_default(#[graphql(default = 100)] arg: i32) -> Option<i32> {
        Some(arg)
    }
}
```
Results in an exposed schema that is similar to this:
```graphql
type Query {
  nonNullDefault(arg: Int = 100): Int
}
```
instead of:
```graphql
type Query {
  nonNullDefault(arg: Int! = 100): Int
}
```

## The GraphQL Spec

Under [Input Coercion](https://spec.graphql.org/draft/#sec-Input-Objects.Input-Coercion) section of the spec, it is specified that the default value should only apply if no value is provided:

> If no value is provided for a defined input object field and that field definition provides a default value, the default value should be used.

And additionally that an explicit `null` is considered semantically different to having not provided a value:

> If the value null was provided for an input object field, and the field’s type is not a non-null type, an entry in the coerced unordered map is given the value null. In other words, there is a semantic difference between the explicitly provided value null versus having not provided a value.

And similarly for variables, the same is true:

> If a variable is provided for an input object field, the runtime value of that variable must be used. If the runtime value is null and the field type is non-null, a field error must be raised. If no runtime value is provided, the variable definition’s default value should be used. If the variable definition does not provide a default value, the input object field definition’s default value should be used.

## Comparison to the reference implementation
I have created a repo here with a suite of tests against both Juniper and graphql-js that can be used for comparison: https://github.com/meiamsome/juniper-default-value

The results of the comparision tests are as follows:
graphql-js:
```
> jest index.test.js

 PASS  ./index.test.js
  ✓ default value (3 ms)
  ✓ literal number (1 ms)
  ✓ literal null
  ✓ variable field default value
  ✓ variable default value
  ✓ variable default value explicit null
  ✓ variable number
  ✓ variable null
  ✓ schema for default non null is non null (1 ms)
```

juniper:
```
running 9 tests
test test::variable_number ... ok
test test::variable_default_value ... ok
test test::default_value ... ok
test test::variable_field_default_value ... ok
test test::literal_null ... FAILED
test test::variable_null ... FAILED
test test::literal_number ... ok
test test::variable_default_value_explicit_null ... FAILED
test test::schema_for_default_non_null_is_non_null ... FAILED

failures:

---- test::literal_null stdout ----
thread 'test::literal_null' panicked at 'assertion failed: `(left == right)`
  left: `Some(100)`,
 right: `None`', src/lib.rs:82:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- test::variable_default_value_explicit_null stdout ----
thread 'test::variable_default_value_explicit_null' panicked at 'assertion failed: `(left == right)`
  left: `Some(100)`,
 right: `None`', src/lib.rs:146:9

---- test::variable_null stdout ----
thread 'test::variable_null' panicked at 'assertion failed: `(left == right)`
  left: `Some(100)`,
 right: `None`', src/lib.rs:190:9

---- test::schema_for_default_non_null_is_non_null stdout ----
thread 'test::schema_for_default_non_null_is_non_null' panicked at 'assertion failed: `(left == right)`
  left: `Object(Object { key_value_list: {"__schema": Object(Object { key_value_list: {"queryType": Object(Object { key_value_list: {"fields": List([Object(Object { key_value_list: {"name": Scalar(String("test")), "args": List([Object(Object { key_value_list: {"name": Scalar(String("arg")), "type": Object(Object { key_value_list: {"name": Scalar(String("Int")), "kind": Scalar(String("SCALAR")), "ofType": Null} })} })])} }), Object(Object { key_value_list: {"name": Scalar(String("nonNullDefault")), "args": List([Object(Object { key_value_list: {"name": Scalar(String("arg")), "type": Object(Object { key_value_list: {"name": Scalar(String("Int")), "kind": Scalar(String("SCALAR")), "ofType": Null} })} })])} })])} })} })} })`,
 right: `Object(Object { key_value_list: {"__schema": Object(Object { key_value_list: {"queryType": Object(Object { key_value_list: {"fields": List([Object(Object { key_value_list: {"name": Scalar(String("test")), "args": List([Object(Object { key_value_list: {"name": Scalar(String("arg")), "type": Object(Object { key_value_list: {"name": Scalar(String("Int")), "kind": Scalar(String("SCALAR")), "ofType": Null} })} })])} }), Object(Object { key_value_list: {"name": Scalar(String("nonNullDefault")), "args": List([Object(Object { key_value_list: {"name": Scalar(String("arg")), "type": Object(Object { key_value_list: {"name": Null, "kind": Scalar(String("NON_NULL")), "ofType": Object(Object { key_value_list: {"name": Scalar(String("Int")), "kind": Scalar(String("SCALAR"))} })} })} })])} })])} })} })} })`', src/lib.rs:301:9


failures:
    test::literal_null
    test::schema_for_default_non_null_is_non_null
    test::variable_default_value_explicit_null
    test::variable_null

test result: FAILED. 5 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```



Discussion / hints:
@meiamsome thanks!
I hope to have time to look at this this week, but I wanted to drop by and say this is an amazing bug report, thank you!
@LegNeato it's OK, we have this on @ilslv's schedule. He will address this in a near time.

It also makes sense to port all tests from graphql-js before releasing 0.16.0. I guess we have much more cases of spec diversion.

Fix patch:
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 0ddfa6200..20e780444 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -48,7 +48,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 - Reworked [`chrono` crate] integration GraphQL scalars according to [graphql-scalars.dev] specs: ([#1010])
     - Disabled `chrono` [Cargo feature] by default.
     - Removed `scalar-naivetime` [Cargo feature].
-- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#528])
+- Removed lifetime parameter from `ParseError`, `GraphlQLError`, `GraphQLBatchRequest` and `GraphQLRequest`. ([#1081], [#528])
 
 ### Added
 
@@ -72,8 +72,10 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 - Unsupported expressions in `graphql_value!` macro. ([#996], [#503])
 - Incorrect GraphQL list coercion rules: `null` cannot be coerced to an `[Int!]!` or `[Int]!`. ([#1004])
 - All procedural macros expansion inside `macro_rules!`. ([#1054], [#1051])
+- Incorrect input value coercion with defaults. ([#1080], [#1073])
 
 [#503]: /../../issues/503
+[#528]: /../../issues/528
 [#750]: /../../issues/750
 [#798]: /../../issues/798
 [#918]: /../../issues/918
@@ -101,6 +103,9 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
 [#1054]: /../../pull/1054
 [#1057]: /../../pull/1057
 [#1060]: /../../pull/1060
+[#1073]: /../../issues/1073
+[#1080]: /../../pull/1080
+[#1081]: /../../pull/1081
 [ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
 
 
diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs
index ea2c0fec2..2336138b6 100644
--- a/juniper/src/ast.rs
+++ b/juniper/src/ast.rs
@@ -295,25 +295,36 @@ impl<S> InputValue<S> {
         Self::Object(o)
     }
 
-    /// Resolve all variables to their values.
+    /// Resolves all variables of this [`InputValue`] to their actual `values`.
+    ///
+    /// If a variable is not present in the `values`:
+    /// - Returns [`None`] in case this is an [`InputValue::Variable`].
+    /// - Skips field in case of an [`InputValue::Object`] field.
+    /// - Replaces with an [`InputValue::Null`] in case of an
+    ///   [`InputValue::List`] element.
+    ///
+    /// This is done, because for an [`InputValue::Variable`] (or an
+    /// [`InputValue::Object`] field) a default value can be used later, if it's
+    /// provided. While on contrary, a single [`InputValue::List`] element
+    /// cannot have a default value.
     #[must_use]
-    pub fn into_const(self, vars: &Variables<S>) -> Self
+    pub fn into_const(self, values: &Variables<S>) -> Option<Self>
     where
         S: Clone,
     {
         match self {
-            Self::Variable(v) => vars.get(&v).map_or_else(InputValue::null, Clone::clone),
-            Self::List(l) => Self::List(
+            Self::Variable(v) => values.get(&v).cloned(),
+            Self::List(l) => Some(Self::List(
                 l.into_iter()
-                    .map(|s| s.map(|v| v.into_const(vars)))
+                    .map(|s| s.map(|v| v.into_const(values).unwrap_or_else(Self::null)))
                     .collect(),
-            ),
-            Self::Object(o) => Self::Object(
+            )),
+            Self::Object(o) => Some(Self::Object(
                 o.into_iter()
-                    .map(|(sk, sv)| (sk, sv.map(|v| v.into_const(vars))))
+                    .filter_map(|(sk, sv)| sv.and_then(|v| v.into_const(values)).map(|sv| (sk, sv)))
                     .collect(),
-            ),
-            v => v,
+            )),
+            v => Some(v),
         }
     }
 
diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs
index 777d80629..887091c7a 100644
--- a/juniper/src/executor/mod.rs
+++ b/juniper/src/executor/mod.rs
@@ -1227,9 +1227,6 @@ impl<'r, S: 'r> Registry<'r, S> {
     }
 
     /// Creates an [`Argument`] with the provided default `value`.
-    ///
-    /// When called with type `T`, the actual [`Argument`] will be given the
-    /// type `Option<T>`.
     pub fn arg_with_default<T>(
         &mut self,
         name: &str,
@@ -1240,7 +1237,7 @@ impl<'r, S: 'r> Registry<'r, S> {
         T: GraphQLType<S> + ToInputValue<S> + FromInputValue<S>,
         S: ScalarValue,
     {
-        Argument::new(name, self.get_type::<Option<T>>(info)).default_value(value.to_input_value())
+        Argument::new(name, self.get_type::<T>(info)).default_value(value.to_input_value())
     }
 
     fn insert_placeholder(&mut self, name: Name, of_type: Type<'r>) {
diff --git a/juniper/src/parser/utils.rs b/juniper/src/parser/utils.rs
index fdad16795..645c30141 100644
--- a/juniper/src/parser/utils.rs
+++ b/juniper/src/parser/utils.rs
@@ -81,7 +81,7 @@ impl<T> Spanning<T> {
         }
     }
 
-    /// Modify the contents of the spanned item
+    /// Modify the contents of the spanned item.
     pub fn map<O, F: Fn(T) -> O>(self, f: F) -> Spanning<O> {
         Spanning {
             item: f(self.item),
@@ -89,6 +89,13 @@ impl<T> Spanning<T> {
             end: self.end,
         }
     }
+
+    /// Modifies the contents of the spanned item in case `f` returns [`Some`],
+    /// or returns [`None`] otherwise.
+    pub fn and_then<O, F: Fn(T) -> Option<O>>(self, f: F) -> Option<Spanning<O>> {
+        let (start, end) = (self.start, self.end);
+        f(self.item).map(|item| Spanning { item, start, end })
+    }
 }
 
 impl<T: fmt::Display> fmt::Display for Spanning<T> {
diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs
index 825bb43aa..09f10bbd7 100644
--- a/juniper/src/schema/schema.rs
+++ b/juniper/src/schema/schema.rs
@@ -211,13 +211,19 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> {
         }
     }
 
-    fn fields(&self, #[graphql(default)] include_deprecated: bool) -> Option<Vec<&Field<S>>> {
+    fn fields(
+        &self,
+        #[graphql(default = false)] include_deprecated: Option<bool>,
+    ) -> Option<Vec<&Field<S>>> {
         match self {
             TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. }))
             | TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => Some(
                 fields
                     .iter()
-                    .filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
+                    .filter(|f| {
+                        include_deprecated.unwrap_or_default()
+                            || !f.deprecation_status.is_deprecated()
+                    })
                     .filter(|f| !f.name.starts_with("__"))
                     .collect(),
             ),
@@ -302,12 +308,18 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> {
         }
     }
 
-    fn enum_values(&self, #[graphql(default)] include_deprecated: bool) -> Option<Vec<&EnumValue>> {
+    fn enum_values(
+        &self,
+        #[graphql(default = false)] include_deprecated: Option<bool>,
+    ) -> Option<Vec<&EnumValue>> {
         match self {
             TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => Some(
                 values
                     .iter()
-                    .filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
+                    .filter(|f| {
+                        include_deprecated.unwrap_or_default()
+                            || !f.deprecation_status.is_deprecated()
+                    })
                     .collect(),
             ),
             _ => None,
diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs
index 80c653cff..ebb948cab 100644
--- a/juniper/src/types/async_await.rs
+++ b/juniper/src/types/async_await.rs
@@ -242,7 +242,9 @@ where
                     f.arguments.as_ref().map(|m| {
                         m.item
                             .iter()
-                            .map(|&(ref k, ref v)| (k.item, v.item.clone().into_const(exec_vars)))
+                            .filter_map(|&(ref k, ref v)| {
+                                v.item.clone().into_const(exec_vars).map(|v| (k.item, v))
+                            })
                             .collect()
                     }),
                     &meta_field.arguments,
diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs
index e43134f10..b46315b72 100644
--- a/juniper/src/types/base.rs
+++ b/juniper/src/types/base.rs
@@ -49,7 +49,6 @@ pub enum TypeKind {
     /// ## Input objects
     ///
     /// Represents complex values provided in queries _into_ the system.
-    #[graphql(name = "INPUT_OBJECT")]
     InputObject,
 
     /// ## List types
@@ -63,7 +62,6 @@ pub enum TypeKind {
     ///
     /// In GraphQL, nullable types are the default. By putting a `!` after a\
     /// type, it becomes non-nullable.
-    #[graphql(name = "NON_NULL")]
     NonNull,
 }
 
@@ -89,7 +87,7 @@ impl<'a, S> Arguments<'a, S> {
         if let (Some(args), Some(meta_args)) = (&mut args, meta_args) {
             for arg in meta_args {
                 let arg_name = arg.name.as_str();
-                if args.get(arg_name).map_or(true, InputValue::is_null) {
+                if args.get(arg_name).is_none() {
                     if let Some(val) = arg.default_value.as_ref() {
                         args.insert(arg_name, val.clone());
                     }
@@ -474,8 +472,8 @@ where
                         f.arguments.as_ref().map(|m| {
                             m.item
                                 .iter()
-                                .map(|&(ref k, ref v)| {
-                                    (k.item, v.item.clone().into_const(exec_vars))
+                                .filter_map(|&(ref k, ref v)| {
+                                    v.item.clone().into_const(exec_vars).map(|v| (k.item, v))
                                 })
                                 .collect()
                         }),
@@ -608,7 +606,7 @@ where
                 .arguments
                 .iter()
                 .flat_map(|m| m.item.get("if"))
-                .flat_map(|v| v.item.clone().into_const(vars).convert())
+                .filter_map(|v| v.item.clone().into_const(vars)?.convert().ok())
                 .next()
                 .unwrap();
 
diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs
index 29140579d..e1f42fdf2 100644
--- a/juniper/src/types/subscriptions.rs
+++ b/juniper/src/types/subscriptions.rs
@@ -316,7 +316,9 @@ where
                     f.arguments.as_ref().map(|m| {
                         m.item
                             .iter()
-                            .map(|&(ref k, ref v)| (k.item, v.item.clone().into_const(exec_vars)))
+                            .filter_map(|&(ref k, ref v)| {
+                                v.item.clone().into_const(exec_vars).map(|v| (k.item, v))
+                            })
                             .collect()
                     }),
                     &meta_field.arguments,
diff --git a/juniper/src/types/utilities.rs b/juniper/src/types/utilities.rs
index 2ed03b1f3..2781418ac 100644
--- a/juniper/src/types/utilities.rs
+++ b/juniper/src/types/utilities.rs
@@ -71,7 +71,7 @@ where
                         let mut remaining_required_fields = input_fields
                             .iter()
                             .filter_map(|f| {
-                                if f.arg_type.is_non_null() {
+                                if f.arg_type.is_non_null() && f.default_value.is_none() {
                                     Some(&f.name)
                                 } else {
                                     None
diff --git a/juniper/src/validation/rules/provided_non_null_arguments.rs b/juniper/src/validation/rules/provided_non_null_arguments.rs
index 7c2cb2664..eb76c1508 100644
--- a/juniper/src/validation/rules/provided_non_null_arguments.rs
+++ b/juniper/src/validation/rules/provided_non_null_arguments.rs
@@ -26,6 +26,7 @@ where
         {
             for meta_arg in meta_args {
                 if meta_arg.arg_type.is_non_null()
+                    && meta_arg.default_value.is_none()
                     && field
                         .item
                         .arguments
diff --git a/juniper_codegen/src/common/default.rs b/juniper_codegen/src/common/default.rs
index 03fb68a06..2354345a1 100644
--- a/juniper_codegen/src/common/default.rs
+++ b/juniper_codegen/src/common/default.rs
@@ -57,9 +57,11 @@ impl ToTokens for Value {
         match self {
             Self::Default => quote! {
                 ::std::default::Default::default()
-            }
-            .to_tokens(into),
-            Self::Expr(expr) => expr.to_tokens(into),
+            },
+            Self::Expr(expr) => quote! {
+                (#expr).into()
+            },
         }
+        .to_tokens(into)
     }
 }


Test patch:
diff --git a/juniper/src/executor_tests/introspection/input_object.rs b/juniper/src/executor_tests/introspection/input_object.rs
index 450f774d7..cf8c0d205 100644
--- a/juniper/src/executor_tests/introspection/input_object.rs
+++ b/juniper/src/executor_tests/introspection/input_object.rs
@@ -462,6 +462,9 @@ async fn field_with_defaults_introspection() {
                 name
                 type {
                     name
+                    ofType {
+                        name
+                    }
                 }
                 defaultValue
             }
@@ -477,12 +480,12 @@ async fn field_with_defaults_introspection() {
         assert_eq!(fields.len(), 2);
         assert!(fields.contains(&graphql_value!({
             "name": "fieldOne",
-            "type": {"name": "Int"},
+            "type": {"name": null, "ofType": {"name": "Int"}},
             "defaultValue": "123",
         })));
         assert!(fields.contains(&graphql_value!({
             "name": "fieldTwo",
-            "type": {"name": "Int"},
+            "type": {"name": null, "ofType": {"name": "Int"}},
             "defaultValue": "456",
         })));
     })
diff --git a/juniper/src/executor_tests/introspection/mod.rs b/juniper/src/executor_tests/introspection/mod.rs
index ed59efc79..e78995ec6 100644
--- a/juniper/src/executor_tests/introspection/mod.rs
+++ b/juniper/src/executor_tests/introspection/mod.rs
@@ -444,9 +444,13 @@ async fn object_introspection() {
             "name": "second",
             "description": "The second number",
             "type": {
-                "name": "Int",
-                "kind": "SCALAR",
-                "ofType": null,
+                "name": null,
+                "kind": "NON_NULL",
+                "ofType": {
+                    "name": "Int",
+                    "kind": "SCALAR",
+                    "ofType": null,
+                },
             },
             "defaultValue": "123",
         }],
diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs
index 023bd61ef..0073d0e2d 100644
--- a/juniper/src/executor_tests/variables.rs
+++ b/juniper/src/executor_tests/variables.rs
@@ -75,6 +75,12 @@ impl TestType {
         format!("{:?}", input)
     }
 
+    fn nullable_field_with_default_argument_value(
+        #[graphql(default = "Hello World".to_owned())] input: Option<String>,
+    ) -> String {
+        format!("{:?}", input)
+    }
+
     fn field_with_nested_object_input(input: Option<TestNestedInputObject>) -> String {
         format!("{:?}", input)
     }
@@ -790,14 +796,29 @@ async fn default_argument_when_not_provided() {
     .await;
 }
 
+#[tokio::test]
+async fn provided_variable_overwrites_default_value() {
+    run_variable_query(
+        r#"query q($input: String!) { fieldWithDefaultArgumentValue(input: $input) }"#,
+        graphql_vars! {"input": "Overwritten"},
+        |result| {
+            assert_eq!(
+                result.get_field_value("fieldWithDefaultArgumentValue"),
+                Some(&graphql_value!(r#""Overwritten""#)),
+            );
+        },
+    )
+    .await;
+}
+
 #[tokio::test]
 async fn default_argument_when_nullable_variable_not_provided() {
     run_query(
-        r#"query q($input: String) { fieldWithDefaultArgumentValue(input: $input) }"#,
+        r#"query q($input: String) { nullableFieldWithDefaultArgumentValue(input: $input) }"#,
         |result| {
             assert_eq!(
-                result.get_field_value("fieldWithDefaultArgumentValue"),
-                Some(&graphql_value!(r#""Hello World""#)),
+                result.get_field_value("nullableFieldWithDefaultArgumentValue"),
+                Some(&graphql_value!(r#"Some("Hello World")"#)),
             );
         },
     )
@@ -805,14 +826,14 @@ async fn default_argument_when_nullable_variable_not_provided() {
 }
 
 #[tokio::test]
-async fn default_argument_when_nullable_variable_set_to_null() {
+async fn null_when_nullable_variable_of_argument_with_default_value_set_to_null() {
     run_variable_query(
-        r#"query q($input: String) { fieldWithDefaultArgumentValue(input: $input) }"#,
+        r#"query q($input: String) { nullableFieldWithDefaultArgumentValue(input: $input) }"#,
         graphql_vars! {"input": null},
         |result| {
             assert_eq!(
-                result.get_field_value("fieldWithDefaultArgumentValue"),
-                Some(&graphql_value!(r#""Hello World""#)),
+                result.get_field_value("nullableFieldWithDefaultArgumentValue"),
+                Some(&graphql_value!(r#"None"#)),
             );
         },
     )
diff --git a/tests/integration/src/codegen/input_object_derive.rs b/tests/integration/src/codegen/input_object_derive.rs
index 085815b74..9f537f92d 100644
--- a/tests/integration/src/codegen/input_object_derive.rs
+++ b/tests/integration/src/codegen/input_object_derive.rs
@@ -1,6 +1,9 @@
 //! Tests for `#[derive(GraphQLInputObject)]` macro.
 
-use juniper::{execute, graphql_object, graphql_value, graphql_vars, GraphQLInputObject};
+use juniper::{
+    execute, graphql_object, graphql_value, graphql_vars, parser::SourcePosition, GraphQLError,
+    GraphQLInputObject, RuleError,
+};
 
 use crate::util::schema;
 
@@ -148,16 +151,189 @@ mod default_value {
 
     #[tokio::test]
     async fn resolves() {
+        const DOC: &str = r#"query q($ve_num: Float!) {
+            literal_implicit_other_number: x(point: { y: 20 })
+            literal_explicit_number: x(point: { x: 20 })
+            literal_implicit_all: x(point: {})
+            variable_explicit_number: x(point: { x: $ve_num })
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &graphql_vars! {"ve_num": 40}, &()).await,
+            Ok((
+                graphql_value!({
+                    "literal_implicit_other_number": 10.0,
+                    "literal_explicit_number": 20.0,
+                    "literal_implicit_all": 10.0,
+                    "variable_explicit_number": 40.0,
+                }),
+                vec![],
+            )),
+        );
+    }
+
+    #[tokio::test]
+    async fn errs_on_explicit_null_literal() {
+        const DOC: &str = r#"{ x(point: { x: 20, y: null }) }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
+            Err(GraphQLError::ValidationError(vec![RuleError::new(
+                "Invalid value for argument \"point\", expected type \"Point2D!\"",
+                &[SourcePosition::new(11, 0, 11)],
+            )]))
+        );
+    }
+
+    #[tokio::test]
+    async fn errs_on_missing_variable() {
+        const DOC: &str = r#"query q($x: Float!){ x(point: { x: $x }) }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
+            Err(GraphQLError::ValidationError(vec![RuleError::new(
+                "Variable \"$x\" of required type \"Float!\" was not provided.",
+                &[SourcePosition::new(8, 0, 8)],
+            )]))
+        );
+    }
+
+    #[tokio::test]
+    async fn is_graphql_input_object() {
+        const DOC: &str = r#"{
+            __type(name: "Point2D") {
+                kind
+            }
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
+            Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])),
+        );
+    }
+
+    #[tokio::test]
+    async fn has_input_fields() {
         const DOC: &str = r#"{
-            x(point: { y: 20 })
-            x2: x(point: { x: 20 })
+            __type(name: "Point2D") {
+                inputFields {
+                    name
+                    description
+                    type {
+                        ofType {
+                            name
+                        }
+                    }
+                    defaultValue
+                }
+            }
         }"#;
 
         let schema = schema(QueryRoot);
 
         assert_eq!(
             execute(DOC, None, &schema, &graphql_vars! {}, &()).await,
-            Ok((graphql_value!({"x": 10.0, "x2": 20.0}), vec![])),
+            Ok((
+                graphql_value!({"__type": {"inputFields": [{
+                    "name": "x",
+                    "description": null,
+                    "type": {"ofType": {"name": "Float"}},
+                    "defaultValue": "10",
+                }, {
+                    "name": "y",
+                    "description": null,
+                    "type": {"ofType": {"name": "Float"}},
+                    "defaultValue": "10",
+                }]}}),
+                vec![],
+            )),
+        );
+    }
+}
+
+mod default_nullable_value {
+    use super::*;
+
+    #[derive(GraphQLInputObject)]
+    struct Point2D {
+        #[graphql(default = 10.0)]
+        x: Option<f64>,
+        #[graphql(default = 10.0)]
+        y: Option<f64>,
+    }
+
+    struct QueryRoot;
+
+    #[graphql_object]
+    impl QueryRoot {
+        fn x(point: Point2D) -> Option<f64> {
+            point.x
+        }
+    }
+
+    #[tokio::test]
+    async fn resolves() {
+        const DOC: &str = r#"query q(
+            $ve_num: Float,
+            $ve_null: Float,
+            $vi: Float,
+            $vde_num: Float = 40,
+            $vde_null: Float = 50,
+            $vdi: Float = 60,
+        ) {
+            literal_implicit_other_number: x(point: { y: 20 })
+            literal_explicit_number: x(point: { x: 20 })
+            literal_implicit_all: x(point: {})
+            literal_explicit_null: x(point: { x: null })
+            literal_implicit_other_null: x(point: { y: null })
+            variable_explicit_number: x(point: { x: $ve_num })
+            variable_explicit_null: x(point: { x: $ve_null })
+            variable_implicit: x(point: { x: $vi })
+            variable_default_explicit_number: x(point: { x: $vde_num })
+            variable_default_explicit_null: x(point: { x: $vde_null })
+            variable_default_implicit: x(point: { x: $vdi })
+        }"#;
+
+        let schema = schema(QueryRoot);
+
+        assert_eq!(
+            execute(
+                DOC,
+                None,
+                &schema,
+                &graphql_vars! {
+                    "ve_num": 30.0,
+                    "ve_null": null,
+                    "vde_num": 100,
+                    "vde_null": null,
+                },
+                &(),
+            )
+            .await,
+            Ok((
+                graphql_value!({
+                    "literal_implicit_other_number": 10.0,
+                    "literal_explicit_number": 20.0,
+                    "literal_implicit_all": 10.0,
+                    "literal_explicit_null": null,
+                    "literal_implicit_other_null": 10.0,
+                    "variable_explicit_number": 30.0,
+                    "variable_explicit_null": null,
+                    "variable_implicit": 10.0,
+                    "variable_default_explicit_number": 100.0,
+                    "variable_default_explicit_null": null,
+                    "variable_default_implicit": 60.0,
+                }),
+                vec![],
+            )),
         );
     }
 
@@ -185,6 +361,7 @@ mod default_value {
                     name
                     description
                     type {
+                        name
                         ofType {
                             name
                         }
@@ -203,13 +380,13 @@ mod default_value {
                     {
                         "name": "x",
                         "description": null,
-                        "type": {"ofType": null},
+                        "type": {"name": "Float", "ofType": null},
                         "defaultValue": "10",
                     },
                     {
                         "name": "y",
                         "description": null,
-                        "type": {"ofType": null},
+                        "type": {"name": "Float", "ofType": null},
                         "defaultValue": "10",
                     },
                 ]}}),
diff --git a/tests/integration/src/codegen/interface_attr_trait.rs b/tests/integration/src/codegen/interface_attr_trait.rs
index 150820fab..7892a027d 100644
--- a/tests/integration/src/codegen/interface_attr_trait.rs
+++ b/tests/integration/src/codegen/interface_attr_trait.rs
@@ -1311,21 +1311,21 @@ mod default_argument {
                     "args": [{
                         "name": "first",
                         "defaultValue": r#""""#,
-                        "type": {"name": "String", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "String"}},
                     }, {
                         "name": "second",
                         "defaultValue": r#""second""#,
-                        "type": {"name": "String", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "String"}},
                     }, {
                         "name": "third",
                         "defaultValue": r#""t""#,
-                        "type": {"name": "String", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "String"}},
                     }],
                 }, {
                     "args": [{
                         "name": "coord",
                         "defaultValue": "{x: 1}",
-                        "type": {"name": "Point", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Point"}},
                     }],
                 }]}}),
                 vec![],
diff --git a/tests/integration/src/codegen/object_attr.rs b/tests/integration/src/codegen/object_attr.rs
index 9161cc55d..5a10710c8 100644
--- a/tests/integration/src/codegen/object_attr.rs
+++ b/tests/integration/src/codegen/object_attr.rs
@@ -1045,10 +1045,10 @@ mod default_argument {
     impl Human {
         fn id(
             #[graphql(default)] arg1: i32,
-            #[graphql(default = "second".to_string())] arg2: String,
+            #[graphql(default = "second".to_string())] arg2: Option<String>,
             #[graphql(default = true)] r#arg3: bool,
         ) -> String {
-            format!("{}|{}&{}", arg1, arg2, r#arg3)
+            format!("{}|{:?}&{}", arg1, arg2, r#arg3)
         }
 
         fn info(#[graphql(default = Point { x: 1 })] coord: Point) -> i32 {
@@ -1069,20 +1069,72 @@ mod default_argument {
     async fn resolves_id_field() {
         let schema = schema(QueryRoot);
 
-        for (input, expected) in &[
-            ("{ human { id } }", "0|second&true"),
-            ("{ human { id(arg1: 1) } }", "1|second&true"),
-            (r#"{ human { id(arg2: "") } }"#, "0|&true"),
-            (r#"{ human { id(arg1: 2, arg2: "") } }"#, "2|&true"),
+        for (input, expected, vars) in &[
+            (
+                "{ human { id } }",
+                r#"0|Some("second")&true"#,
+                graphql_vars! {},
+            ),
+            (
+                "{ human { id(arg1: 1) } }",
+                r#"1|Some("second")&true"#,
+                graphql_vars! {},
+            ),
+            (
+                r#"{ human { id(arg2: "other") } }"#,
+                r#"0|Some("other")&true"#,
+                graphql_vars! {},
+            ),
+            (
+                "{ human { id(arg2: null) } }",
+                r#"0|None&true"#,
+                graphql_vars! {},
+            ),
+            (
+                "query q($arg2: String) { human { id(arg2: $arg2) } }",
+                r#"0|Some("second")&true"#,
+                graphql_vars! {},
+            ),
+            (
+                "query q($arg2: String) { human{ id(arg2: $arg2) } }",
+                r#"0|None&true"#,
+                graphql_vars! { "arg2": null },
+            ),
+            (
+                "query q($arg2: String) { human{ id(arg2: $arg2) } }",
+                r#"0|Some("other")&true"#,
+                graphql_vars! { "arg2": "other" },
+            ),
+            (
+                r#"query q($arg2: String = "other") { human { id(arg2: $arg2) } }"#,
+                r#"0|Some("other")&true"#,
+                graphql_vars! {},
+            ),
+            (
+                r#"query q($arg2: String = "other") { human { id(arg2: $arg2) } }"#,
+                r#"0|None&true"#,
+                graphql_vars! { "arg2": null },
+            ),
+            (
+                r#"query q($arg2: String = "other") { human { id(arg2: $arg2) } }"#,
+                r#"0|Some("hello")&true"#,
+                graphql_vars! { "arg2": "hello" },
+            ),
+            (
+                r#"{ human { id(arg1: 2, arg2: "") } }"#,
+                r#"2|Some("")&true"#,
+                graphql_vars! {},
+            ),
             (
                 r#"{ human { id(arg1: 1, arg2: "", arg3: false) } }"#,
-                "1|&false",
+                r#"1|Some("")&false"#,
+                graphql_vars! {},
             ),
         ] {
             let expected: &str = *expected;
 
             assert_eq!(
-                execute(*input, None, &schema, &graphql_vars! {}, &()).await,
+                execute(*input, None, &schema, &vars, &(),).await,
                 Ok((graphql_value!({"human": {"id": expected}}), vec![])),
             );
         }
@@ -1133,7 +1185,7 @@ mod default_argument {
                     "args": [{
                         "name": "arg1",
                         "defaultValue": "0",
-                        "type": {"name": "Int", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Int"}},
                     }, {
                         "name": "arg2",
                         "defaultValue": r#""second""#,
@@ -1141,13 +1193,13 @@ mod default_argument {
                     }, {
                         "name": "arg3",
                         "defaultValue": "true",
-                        "type": {"name": "Boolean", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Boolean"}},
                     }],
                 }, {
                     "args": [{
                         "name": "coord",
                         "defaultValue": "{x: 1}",
-                        "type": {"name": "Point", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Point"}},
                     }],
                 }]}}),
                 vec![],
diff --git a/tests/integration/src/codegen/subscription_attr.rs b/tests/integration/src/codegen/subscription_attr.rs
index 306b8c8e3..47535fab0 100644
--- a/tests/integration/src/codegen/subscription_attr.rs
+++ b/tests/integration/src/codegen/subscription_attr.rs
@@ -607,21 +607,21 @@ mod default_argument {
                     "args": [{
                         "name": "arg1",
                         "defaultValue": "0",
-                        "type": {"name": "Int", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Int"}},
                     }, {
                         "name": "arg2",
                         "defaultValue": r#""second""#,
-                        "type": {"name": "String", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "String"}},
                     }, {
                         "name": "arg3",
                         "defaultValue": "true",
-                        "type": {"name": "Boolean", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Boolean"}},
                     }],
                 }, {
                     "args": [{
                         "name": "coord",
                         "defaultValue": "{x: 1}",
-                        "type": {"name": "Point", "ofType": null},
+                        "type": {"name": null, "ofType": {"name": "Point"}},
                     }],
                 }]}}),
                 vec![],