diff --git a/docs/book/content/SUMMARY.md b/docs/book/content/SUMMARY.md
index 49c542f5..24ea171f 100644
--- a/docs/book/content/SUMMARY.md
+++ b/docs/book/content/SUMMARY.md
@@ -29,6 +29,7 @@
- [Introspection](advanced/introspection.md)
- [Non-struct objects](advanced/non_struct_objects.md)
+ - [Implicit and explicit null](advanced/implicit_and_explicit_null.md)
- [Objects and generics](advanced/objects_and_generics.md)
- [Multiple operations per request](advanced/multiple_ops_per_request.md)
- [Dataloaders](advanced/dataloaders.md)
diff --git a/docs/book/content/advanced/implicit_and_explicit_null.md b/docs/book/content/advanced/implicit_and_explicit_null.md
new file mode 100644
index 00000000..014249a1
--- /dev/null
+++ b/docs/book/content/advanced/implicit_and_explicit_null.md
@@ -0,0 +1,116 @@
+# Implicit and explicit null
+
+There are two ways that a client can submit a null argument or field in a query.
+
+They can use a null literal:
+
+```graphql
+{
+ field(arg: null)
+}
+```
+
+Or they can simply omit the argument:
+
+```graphql
+{
+ field
+}
+```
+
+The former is an explicit null and the latter is an implicit null.
+
+There are some situations where it's useful to know which one the user provided.
+
+For example, let's say your business logic has a function that allows users to
+perform a "patch" operation on themselves. Let's say your users can optionally
+have favorite and least favorite numbers, and the input for that might look
+like this:
+
+```rust
+/// Updates user attributes. Fields that are `None` are left as-is.
+pub struct UserPatch {
+ /// If `Some`, updates the user's favorite number.
+ pub favorite_number: Option>,
+
+ /// If `Some`, updates the user's least favorite number.
+ pub least_favorite_number: Option >,
+}
+
+# fn main() {}
+```
+
+To set a user's favorite number to 7, you would set `favorite_number` to
+`Some(Some(7))`. In GraphQL, that might look like this:
+
+```graphql
+mutation { patchUser(patch: { favoriteNumber: 7 }) }
+```
+
+To unset the user's favorite number, you would set `favorite_number` to
+`Some(None)`. In GraphQL, that might look like this:
+
+```graphql
+mutation { patchUser(patch: { favoriteNumber: null }) }
+```
+
+If you want to leave the user's favorite number alone, you would set it to
+`None`. In GraphQL, that might look like this:
+
+```graphql
+mutation { patchUser(patch: {}) }
+```
+
+The last two cases rely on being able to distinguish between explicit and implicit null.
+
+In Juniper, this can be done using the `Nullable` type:
+
+```rust
+# extern crate juniper;
+use juniper::{FieldResult, Nullable};
+
+#[derive(juniper::GraphQLInputObject)]
+struct UserPatchInput {
+ pub favorite_number: Nullable,
+ pub least_favorite_number: Nullable,
+}
+
+impl Into for UserPatchInput {
+ fn into(self) -> UserPatch {
+ UserPatch {
+ // The `explicit` function transforms the `Nullable` into an
+ // `Option>` as expected by the business logic layer.
+ favorite_number: self.favorite_number.explicit(),
+ least_favorite_number: self.least_favorite_number.explicit(),
+ }
+ }
+}
+
+# pub struct UserPatch {
+# pub favorite_number: Option >,
+# pub least_favorite_number: Option >,
+# }
+
+# struct Session;
+# impl Session {
+# fn patch_user(&self, _patch: UserPatch) -> FieldResult<()> { Ok(()) }
+# }
+
+struct Context {
+ session: Session,
+}
+
+struct Mutation;
+
+#[juniper::graphql_object(Context=Context)]
+impl Mutation {
+ fn patch_user(ctx: &Context, patch: UserPatchInput) -> FieldResult {
+ ctx.session.patch_user(patch.into())?;
+ Ok(true)
+ }
+}
+# fn main() {}
+```
+
+This type functions much like `Option`, but has two empty variants so you can
+distinguish between implicit and explicit null.
diff --git a/docs/book/content/advanced/index.md b/docs/book/content/advanced/index.md
index e46978b3..8db84fc6 100644
--- a/docs/book/content/advanced/index.md
+++ b/docs/book/content/advanced/index.md
@@ -4,7 +4,8 @@ The chapters below cover some more advanced scenarios.
- [Introspection](introspection.md)
- [Non-struct objects](non_struct_objects.md)
+- [Implicit and explicit null](implicit_and_explicit_null.md)
- [Objects and generics](objects_and_generics.md)
- [Multiple operations per request](multiple_ops_per_request.md)
- [Dataloaders](dataloaders.md)
-- [Subscriptions](subscriptions.md)
\ No newline at end of file
+- [Subscriptions](subscriptions.md)
diff --git a/integration_tests/juniper_tests/src/explicit_null.rs b/integration_tests/juniper_tests/src/explicit_null.rs
new file mode 100644
index 00000000..3ade4b52
--- /dev/null
+++ b/integration_tests/juniper_tests/src/explicit_null.rs
@@ -0,0 +1,81 @@
+use juniper::*;
+
+pub struct Context;
+
+impl juniper::Context for Context {}
+
+pub struct Query;
+
+#[derive(juniper::GraphQLInputObject)]
+struct ObjectInput {
+ field: Nullable,
+}
+
+#[graphql_object(Context=Context)]
+impl Query {
+ fn is_explicit_null(arg: Nullable) -> bool {
+ arg.is_explicit_null()
+ }
+
+ fn object_field_is_explicit_null(obj: ObjectInput) -> bool {
+ obj.field.is_explicit_null()
+ }
+}
+
+type Schema = juniper::RootNode<'static, Query, EmptyMutation, EmptySubscription>;
+
+#[tokio::test]
+async fn explicit_null() {
+ let ctx = Context;
+
+ let query = r#"
+ query Foo($emptyObj: ObjectInput!, $literalNullObj: ObjectInput!) {
+ literalOneIsExplicitNull: isExplicitNull(arg: 1)
+ literalNullIsExplicitNull: isExplicitNull(arg: null)
+ noArgIsExplicitNull: isExplicitNull
+ literalOneFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: 1})
+ literalNullFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {field: null})
+ noFieldIsExplicitNull: objectFieldIsExplicitNull(obj: {})
+ emptyVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $emptyObj)
+ literalNullVariableObjectFieldIsExplicitNull: objectFieldIsExplicitNull(obj: $literalNullObj)
+ }
+ "#;
+
+ let (data, errors) = juniper::execute(
+ query,
+ None,
+ &Schema::new(
+ Query,
+ EmptyMutation::::new(),
+ EmptySubscription::::new(),
+ ),
+ &[
+ ("emptyObj".to_string(), InputValue::Object(vec![])),
+ (
+ "literalNullObj".to_string(),
+ InputValue::object(vec![("field", InputValue::null())].into_iter().collect()),
+ ),
+ ]
+ .iter()
+ .cloned()
+ .collect(),
+ &ctx,
+ )
+ .await
+ .unwrap();
+
+ assert_eq!(errors.len(), 0);
+ assert_eq!(
+ data,
+ graphql_value!({
+ "literalOneIsExplicitNull": false,
+ "literalNullIsExplicitNull": true,
+ "noArgIsExplicitNull": false,
+ "literalOneFieldIsExplicitNull": false,
+ "literalNullFieldIsExplicitNull": true,
+ "noFieldIsExplicitNull": false,
+ "emptyVariableObjectFieldIsExplicitNull": false,
+ "literalNullVariableObjectFieldIsExplicitNull": true,
+ })
+ );
+}
diff --git a/integration_tests/juniper_tests/src/lib.rs b/integration_tests/juniper_tests/src/lib.rs
index 17105046..d41a0c07 100644
--- a/integration_tests/juniper_tests/src/lib.rs
+++ b/integration_tests/juniper_tests/src/lib.rs
@@ -5,6 +5,8 @@ mod codegen;
#[cfg(test)]
mod custom_scalar;
#[cfg(test)]
+mod explicit_null;
+#[cfg(test)]
mod issue_371;
#[cfg(test)]
mod issue_398;
diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md
index 762dbb21..b0026181 100644
--- a/juniper/CHANGELOG.md
+++ b/juniper/CHANGELOG.md
@@ -34,6 +34,8 @@
- `RuleError`
- Support `chrono-tz::Tz` scalar behind a `chrono-tz` feature flag. ([#519](https://github.com/graphql-rust/juniper/pull/519))
+
+- Added support for distinguishing between between implicit and explicit null. ([#795](https://github.com/graphql-rust/juniper/pull/795))
## Fixes
diff --git a/juniper/src/ast.rs b/juniper/src/ast.rs
index 9ad29d9e..df32ecec 100644
--- a/juniper/src/ast.rs
+++ b/juniper/src/ast.rs
@@ -153,6 +153,14 @@ pub type Document<'a, S> = Vec>;
pub trait FromInputValue: Sized {
/// Performs the conversion.
fn from_input_value(v: &InputValue) -> Option;
+
+ /// Performs the conversion from an absent value (e.g. to distinguish between implicit and
+ /// explicit null). The default implementation just uses `from_input_value` as if an explicit
+ /// null were provided. This conversion must not fail.
+ fn from_implicit_null() -> Self {
+ Self::from_input_value(&InputValue::::Null)
+ .expect("input value conversion from null must not fail")
+ }
}
/// Losslessly clones a Rust data type into an InputValue.
diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs
index 7456ef2c..3cab9b96 100644
--- a/juniper/src/lib.rs
+++ b/juniper/src/lib.rs
@@ -169,6 +169,7 @@ pub use crate::{
async_await::{DynGraphQLValueAsync, GraphQLTypeAsync, GraphQLValueAsync},
base::{Arguments, DynGraphQLValue, GraphQLType, GraphQLValue, TypeKind},
marker::{self, GraphQLInterface, GraphQLUnion},
+ nullable::Nullable,
scalars::{EmptyMutation, EmptySubscription, ID},
subscriptions::{
ExecutionOutput, GraphQLSubscriptionType, GraphQLSubscriptionValue,
diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs
index 46072ee6..1b395d44 100644
--- a/juniper/src/types/base.rs
+++ b/juniper/src/types/base.rs
@@ -91,8 +91,6 @@ where
if !args.contains_key(arg.name.as_str()) || args[arg.name.as_str()].is_null() {
if let Some(ref default_value) = arg.default_value {
args.insert(arg.name.as_str(), default_value.clone());
- } else {
- args.insert(arg.name.as_str(), InputValue::null());
}
}
}
diff --git a/juniper/src/types/mod.rs b/juniper/src/types/mod.rs
index c6575fc9..8ac005ba 100644
--- a/juniper/src/types/mod.rs
+++ b/juniper/src/types/mod.rs
@@ -3,6 +3,7 @@ pub mod base;
pub mod containers;
pub mod marker;
pub mod name;
+pub mod nullable;
pub mod pointers;
pub mod scalars;
pub mod subscriptions;
diff --git a/juniper/src/types/nullable.rs b/juniper/src/types/nullable.rs
new file mode 100644
index 00000000..9436e842
--- /dev/null
+++ b/juniper/src/types/nullable.rs
@@ -0,0 +1,328 @@
+use crate::{
+ ast::{FromInputValue, InputValue, Selection, ToInputValue},
+ executor::{ExecutionResult, Executor, Registry},
+ schema::meta::MetaType,
+ types::{
+ async_await::GraphQLValueAsync,
+ base::{GraphQLType, GraphQLValue},
+ marker::IsInputType,
+ },
+ value::{ScalarValue, Value},
+};
+
+/// `Nullable` can be used in situations where you need to distinguish between an implicitly and
+/// explicitly null input value.
+///
+/// The GraphQL spec states that these two field calls are similar, but are not identical:
+///
+/// ```graphql
+/// {
+/// field(arg: null)
+/// field
+/// }
+/// ```
+///
+/// The first has explicitly provided null to the argument “arg”, while the second has implicitly
+/// not provided a value to the argument “arg”. These two forms may be interpreted differently. For
+/// example, a mutation representing deleting a field vs not altering a field, respectively.
+///
+/// In cases where you do not need to be able to distinguish between the two types of null, you
+/// should simply use `Option`.
+#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub enum Nullable {
+ /// No value
+ ImplicitNull,
+ /// No value, explicitly specified to be null
+ ExplicitNull,
+ /// Some value `T`
+ Some(T),
+}
+
+impl Default for Nullable {
+ fn default() -> Self {
+ Self::ImplicitNull
+ }
+}
+
+impl Nullable {
+ /// Returns `true` if the nullable is a `ExplicitNull` value.
+ #[inline]
+ pub fn is_explicit_null(&self) -> bool {
+ match self {
+ Self::ExplicitNull => true,
+ _ => false,
+ }
+ }
+
+ /// Returns `true` if the nullable is a `ImplicitNull` value.
+ #[inline]
+ pub fn is_implicit_null(&self) -> bool {
+ match self {
+ Self::ImplicitNull => true,
+ _ => false,
+ }
+ }
+
+ /// Returns `true` if the nullable is a `Some` value.
+ #[inline]
+ pub fn is_some(&self) -> bool {
+ match self {
+ Self::Some(_) => true,
+ _ => false,
+ }
+ }
+
+ /// Returns `true` if the nullable is not a `Some` value.
+ #[inline]
+ pub fn is_null(&self) -> bool {
+ match self {
+ Self::Some(_) => false,
+ _ => true,
+ }
+ }
+
+ /// Converts from `&mut Nullable` to `Nullable<&mut T>`.
+ #[inline]
+ pub fn as_mut(&mut self) -> Nullable<&mut T> {
+ match *self {
+ Self::Some(ref mut x) => Nullable::Some(x),
+ Self::ImplicitNull => Nullable::ImplicitNull,
+ Self::ExplicitNull => Nullable::ExplicitNull,
+ }
+ }
+
+ /// Returns the contained `Some` value, consuming the `self` value.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the value is not a `Some` with a custom panic message provided by `msg`.
+ #[inline]
+ #[track_caller]
+ pub fn expect(self, msg: &str) -> T {
+ self.some().expect(msg)
+ }
+
+ /// Returns the contained `Some` value or a provided default.
+ #[inline]
+ pub fn unwrap_or(self, default: T) -> T {
+ self.some().unwrap_or(default)
+ }
+
+ /// Returns the contained `Some` value or computes it from a closure.
+ #[inline]
+ pub fn unwrap_or_else T>(self, f: F) -> T {
+ self.some().unwrap_or_else(f)
+ }
+
+ /// Maps a `Nullable` to `Nullable` by applying a function to a contained value.
+ #[inline]
+ pub fn map U>(self, f: F) -> Nullable {
+ match self {
+ Self::Some(x) => Nullable::Some(f(x)),
+ Self::ImplicitNull => Nullable::ImplicitNull,
+ Self::ExplicitNull => Nullable::ExplicitNull,
+ }
+ }
+
+ /// Applies a function to the contained value (if any), or returns the provided default (if
+ /// not).
+ #[inline]
+ pub fn map_or U>(self, default: U, f: F) -> U {
+ self.some().map_or(default, f)
+ }
+
+ /// Applies a function to the contained value (if any), or computes a default (if not).
+ #[inline]
+ pub fn map_or_else U, F: FnOnce(T) -> U>(self, default: D, f: F) -> U {
+ self.some().map_or_else(default, f)
+ }
+
+ /// Transforms the `Nullable` into a `Result`, mapping `Some(v)` to `Ok(v)` and
+ /// `ImplicitNull` or `ExplicitNull` to `Err(err)`.
+ #[inline]
+ pub fn ok_or(self, err: E) -> Result {
+ self.some().ok_or(err)
+ }
+
+ /// Transforms the `Nullable` into a `Result`, mapping `Some(v)` to `Ok(v)` and
+ /// `ImplicitNull` or `ExplicitNull` to `Err(err())`.
+ #[inline]
+ pub fn ok_or_else E>(self, err: F) -> Result {
+ self.some().ok_or_else(err)
+ }
+
+ /// Returns the nullable if it contains a value, otherwise returns `b`.
+ #[inline]
+ pub fn or(self, b: Self) -> Self {
+ match self {
+ Self::Some(_) => self,
+ _ => b,
+ }
+ }
+
+ /// Returns the nullable if it contains a value, otherwise calls `f` and
+ /// returns the result.
+ #[inline]
+ pub fn or_else Nullable>(self, f: F) -> Nullable {
+ match self {
+ Self::Some(_) => self,
+ _ => f(),
+ }
+ }
+
+ /// Replaces the actual value in the nullable by the value given in parameter, returning the
+ /// old value if present, leaving a `Some` in its place without deinitializing either one.
+ #[inline]
+ pub fn replace(&mut self, value: T) -> Self {
+ std::mem::replace(self, Self::Some(value))
+ }
+
+ /// Converts from `Nullable` to `Option`.
+ pub fn some(self) -> Option {
+ match self {
+ Self::Some(v) => Some(v),
+ _ => None,
+ }
+ }
+
+ /// Converts from `Nullable` to `Option>`, mapping `Some(v)` to `Some(Some(v))`,
+ /// `ExplicitNull` to `Some(None)`, and `ImplicitNull` to `None`.
+ pub fn explicit(self) -> Option > {
+ match self {
+ Self::Some(v) => Some(Some(v)),
+ Self::ExplicitNull => Some(None),
+ Self::ImplicitNull => None,
+ }
+ }
+}
+
+impl Nullable<&T> {
+ /// Maps a `Nullable<&T>` to a `Nullable` by copying the contents of the nullable.
+ pub fn copied(self) -> Nullable {
+ self.map(|&t| t)
+ }
+}
+
+impl Nullable<&mut T> {
+ /// Maps a `Nullable<&mut T>` to a `Nullable` by copying the contents of the nullable.
+ pub fn copied(self) -> Nullable {
+ self.map(|&mut t| t)
+ }
+}
+
+impl Nullable<&T> {
+ /// Maps a `Nullable<&T>` to a `Nullable` by cloning the contents of the nullable.
+ pub fn cloned(self) -> Nullable {
+ self.map(|t| t.clone())
+ }
+}
+
+impl Nullable<&mut T> {
+ /// Maps a `Nullable<&mut T>` to a `Nullable` by cloning the contents of the nullable.
+ pub fn cloned(self) -> Nullable {
+ self.map(|t| t.clone())
+ }
+}
+
+impl GraphQLType for Nullable
+where
+ T: GraphQLType,
+ S: ScalarValue,
+{
+ fn name(_: &Self::TypeInfo) -> Option<&'static str> {
+ None
+ }
+
+ fn meta<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> MetaType<'r, S>
+ where
+ S: 'r,
+ {
+ registry.build_nullable_type::(info).into_meta()
+ }
+}
+
+impl GraphQLValue for Nullable
+where
+ S: ScalarValue,
+ T: GraphQLValue,
+{
+ type Context = T::Context;
+ type TypeInfo = T::TypeInfo;
+
+ fn type_name(&self, _: &Self::TypeInfo) -> Option<&'static str> {
+ None
+ }
+
+ fn resolve(
+ &self,
+ info: &Self::TypeInfo,
+ _: Option<&[Selection]>,
+ executor: &Executor,
+ ) -> ExecutionResult {
+ match *self {
+ Self::Some(ref obj) => executor.resolve(info, obj),
+ _ => Ok(Value::null()),
+ }
+ }
+}
+
+impl GraphQLValueAsync for Nullable
+where
+ T: GraphQLValueAsync,
+ T::TypeInfo: Sync,
+ T::Context: Sync,
+ S: ScalarValue + Send + Sync,
+{
+ fn resolve_async<'a>(
+ &'a self,
+ info: &'a Self::TypeInfo,
+ _: Option<&'a [Selection]>,
+ executor: &'a Executor,
+ ) -> crate::BoxFuture<'a, ExecutionResult> {
+ let f = async move {
+ let value = match self {
+ Self::Some(obj) => executor.resolve_into_value_async(info, obj).await,
+ _ => Value::null(),
+ };
+ Ok(value)
+ };
+ Box::pin(f)
+ }
+}
+
+impl FromInputValue for Nullable
+where
+ T: FromInputValue,
+ S: ScalarValue,
+{
+ fn from_input_value(v: &InputValue) -> Option> {
+ match v {
+ &InputValue::Null => Some(Self::ExplicitNull),
+ v => v.convert().map(Self::Some),
+ }
+ }
+
+ fn from_implicit_null() -> Self {
+ Self::ImplicitNull
+ }
+}
+
+impl ToInputValue for Nullable
+where
+ T: ToInputValue,
+ S: ScalarValue,
+{
+ fn to_input_value(&self) -> InputValue {
+ match *self {
+ Self::Some(ref v) => v.to_input_value(),
+ _ => InputValue::null(),
+ }
+ }
+}
+
+impl IsInputType for Nullable
+where
+ T: IsInputType,
+ S: ScalarValue,
+{
+}
diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs
index b594df5e..ed90f932 100644
--- a/juniper_codegen/src/impl_object.rs
+++ b/juniper_codegen/src/impl_object.rs
@@ -46,6 +46,13 @@ fn create(
let top_attrs = &_impl.attrs;
+ let scalar = _impl
+ .attrs
+ .scalar
+ .as_ref()
+ .map(|s| quote!( #s ))
+ .unwrap_or_else(|| quote!(::juniper::DefaultScalarValue));
+
let fields = _impl
.methods
.iter()
@@ -87,10 +94,6 @@ fn create(
.apply(&arg_name)
});
- let expect_text = format!(
- "Internal error: missing argument {} - validation must have failed",
- &final_name
- );
let mut_modifier = if is_mut { quote!(mut) } else { quote!() };
if final_name.starts_with("__") {
@@ -109,7 +112,7 @@ fn create(
let resolver = quote!(
let #mut_modifier #arg_ident = args
.get::<#ty>(#final_name)
- .expect(#expect_text);
+ .unwrap_or_else(::juniper::FromInputValue::<#scalar>::from_implicit_null);
);
let field_type = util::GraphQLTypeDefinitionFieldArg {
diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs
index bd82ed44..f5459280 100644
--- a/juniper_codegen/src/util/mod.rs
+++ b/juniper_codegen/src/util/mod.rs
@@ -1701,34 +1701,35 @@ impl GraphQLTypeDefiniton {
})
.collect::>();
- let from_inputs = self.fields.iter().map(|field| {
- let field_ident = &field.resolver_code;
- let field_name = &field.name;
+ let from_inputs = self
+ .fields
+ .iter()
+ .map(|field| {
+ let field_ident = &field.resolver_code;
+ let field_name = &field.name;
- // Build from_input clause.
- let from_input_default = match field.default {
- Some(ref def) => {
- quote! {
- Some(&&::juniper::InputValue::Null) | None if true => #def,
+ // Build from_input clause.
+ let from_input_default = match field.default {
+ Some(ref def) => {
+ quote! {
+ Some(&&::juniper::InputValue::Null) | None if true => #def,
+ }
}
- }
- None => quote! {},
- };
+ None => quote! {},
+ };
- quote!(
- #field_ident: {
- // TODO: investigate the unwraps here, they seem dangerous!
- match obj.get(#field_name) {
- #from_input_default
- Some(ref v) => ::juniper::FromInputValue::from_input_value(v).unwrap(),
- None => {
- ::juniper::FromInputValue::from_input_value(&::juniper::InputValue::<#scalar>::null())
- .unwrap()
- },
- }
- },
- )
- }).collect::>();
+ quote!(
+ #field_ident: {
+ // TODO: investigate the unwraps here, they seem dangerous!
+ match obj.get(#field_name) {
+ #from_input_default
+ Some(ref v) => ::juniper::FromInputValue::from_input_value(v).unwrap(),
+ None => ::juniper::FromInputValue::<#scalar>::from_implicit_null(),
+ }
+ },
+ )
+ })
+ .collect::>();
let to_inputs = self
.fields