Revert ArgumentValue->FieldValue changes

See gh-1187
See gh-1190
See gh-1174
This commit is contained in:
Brian Clozel
2025-04-15 12:01:40 +02:00
parent 179929a298
commit ac9616fd9f
34 changed files with 105 additions and 1181 deletions

View File

@@ -393,47 +393,6 @@ include-code::UseInterceptor[tag=register,indent=0]
[[client.fieldvalue]]
== `FieldValue`
By default, input types in GraphQL are nullable and optional, an input value (or any of its fields)
can be set to the `null` literal, or not provided at all. This distinction is useful for
partial updates with a mutation where the underlying data may also be, either set to
`null` or not changed at all accordingly.
Similar to the xref:controllers.adoc#controllers.schema-mapping.fieldvalue[`FieldValue<T> support in controllers`],
we can wrap an Input type with `FieldValue<T>` or use it at the level of class attributes on the client side.
Given a `ProjectInput` class like:
include-code::ProjectInput[indent=0]
We can use our client to send a mutation request:
include-code::FieldValueClient[tag=fieldvalue,indent=0]
For this to work, the client must use Jackson for JSON (de)serialization and must be configured
with the `org.springframework.graphql.client.json.GraphQlModule`.
This can be registered manually on the underlying HTTP client like so:
include-code::FieldValueClient[tag=createclient,indent=0]
This `GraphQlModule` can be globally registered in Spring Boot applications by contributing it as a bean:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class GraphQlJsonConfiguration {
@Bean
public GraphQLModule graphQLModule() {
return new GraphQLModule();
}
}
----
[[client.dgsgraphqlclient]]
== DGS Codegen

View File

@@ -143,11 +143,11 @@ See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
See xref:controllers.adoc#controllers.schema-mapping.argument[`@Argument`].
| `FieldValue`
| `ArgumentValue`
| For access to a named field argument bound to a higher-level, typed Object along
with a flag to indicate if the input argument was omitted vs set to `null`.
See xref:controllers.adoc#controllers.schema-mapping.field-value[`FieldValue`].
See xref:controllers.adoc#controllers.schema-mapping.argument-value[`ArgumentValue`].
| `@Arguments`
| For access to all field arguments bound to a higher-level, typed Object.
@@ -403,8 +403,8 @@ specified in the annotation, or to the parameter name. For access to the full ar
map, please use xref:controllers.adoc#controllers.schema-mapping.arguments[`@Arguments`] instead.
[[controllers.schema-mapping.fieldvalue]]
=== `FieldValue`
[[controllers.schema-mapping.argument-value]]
=== `ArgumentValue`
By default, input arguments in GraphQL are nullable and optional, which means an argument
can be set to the `null` literal, or not provided at all. This distinction is useful for
@@ -414,7 +414,7 @@ there is no way to make such a distinction, because you would get `null` or an e
`Optional` in both cases.
If you want to know not whether a value was not provided at all, you can declare an
`FieldValue` method parameter, which is a simple container for the resulting value,
`ArgumentValue` method parameter, which is a simple container for the resulting value,
along with a flag to indicate whether the input argument was omitted altogether. You
can use this instead of `@Argument`, in which case the argument name is determined from
the method parameter name, or together with `@Argument` to specify the argument name.
@@ -426,31 +426,20 @@ For example:
@Controller
public class BookController {
@QueryMapping
public List<Book> searchBook(@Argument String search, FieldValue<Genre> genre) {
if (!genre.isOmitted()) {
// genre has been set but might hold a "null" value
Genre genreValue = genre.value();
}
}
@MutationMapping
public void addBook(@Argument BookInput bookInput) {
FieldValue<String> genre = bookInput.genre();
genre.ifPresent(genre -> {
//...
});
public void addBook(ArgumentValue<BookInput> bookInput) {
if (!bookInput.isOmitted()) {
BookInput value = bookInput.value();
// ...
}
}
}
----
`FieldValue` is also supported as a field within the object structure of an `@Argument`
`ArgumentValue` is also supported as a field within the object structure of an `@Argument`
method parameter, either initialized via a constructor argument or via a setter, including
as a field of an object nested at any level below the top level object.
This is also supported on the client side with a dedicated Jackson Module,
see the xref:client.adoc#client.fieldvalue[`FieldValue` support for clients] section.
[[controllers.schema-mapping.arguments]]
=== `@Arguments`

View File

@@ -1,66 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.docs.client.fieldvalue;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.client.ClientGraphQlResponse;
import org.springframework.graphql.client.HttpGraphQlClient;
import org.springframework.graphql.client.json.GraphQlModule;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.reactive.function.client.WebClient;
public class FieldValueClient {
private final HttpGraphQlClient graphQlClient;
// tag::createclient[]
public FieldValueClient(HttpGraphQlClient graphQlClient) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.modulesToInstall(new GraphQlModule())
.build();
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON);
WebClient webClient = WebClient.builder()
.baseUrl("https://example.com/graphql")
.codecs((codecs) -> codecs.defaultCodecs().jackson2JsonEncoder(jsonEncoder))
.build();
this.graphQlClient = HttpGraphQlClient.create(webClient);
}
// end::createclient[]
// tag::fieldvalue[]
public void updateProject() {
ProjectInput projectInput = new ProjectInput("spring-graphql",
FieldValue.ofNullable("Spring for GraphQL"));
ClientGraphQlResponse response = this.graphQlClient.document("""
mutation updateProject($project: ProjectInput!) {
updateProject($project: $project) {
id
name
}
}
""")
.variables(Map.of("project", projectInput))
.executeSync();
}
// end::fieldvalue[]
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.docs.client.fieldvalue;
import org.springframework.graphql.FieldValue;
public record ProjectInput(String id, FieldValue<String> name) {
}

View File

@@ -1,172 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Simple container for GraphQL field values that indicates if the field
* value is present, provided but set to {@literal "null"} or omitted altogether.
*
* <p>In the case of GraphQL mutations, clients send an Input Object with several fields;
* the server must understand the difference between a field being absent
* (the existing value should be left as-is) and a field set to {@literal "null"}
* (the existing value must be set to {@literal "null"}). {@code FieldValue<T>}
* helps to make this distinction.
*
* <p>Supported in one of the following places:
* <ul>
* <li>On a controller method parameter, either instead of
* {@link org.springframework.graphql.data.method.annotation.Argument @Argument}
* in which case the argument name is determined from the method parameter name,
* or together with {@code @Argument} to specify the argument name.
* <li>As a field within the object structure of an {@code @Argument} method
* parameter, either initialized via a constructor argument or a setter,
* including as a field of an object nested at any level below the top level
* object.
* </ul>
*
* @param <T> the type of value contained
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 1.4.0
* @see <a href="http://spec.graphql.org/October2021/#sec-Input-Objects">Input Object</a>
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
*/
public final class FieldValue<T> {
private static final FieldValue<?> EMPTY = new FieldValue<>(null, false);
private static final FieldValue<?> OMITTED = new FieldValue<>(null, true);
@Nullable
private final T value;
private final boolean omitted;
private FieldValue(@Nullable T value, boolean omitted) {
this.value = value;
this.omitted = omitted;
}
/**
* Return {@code true} if a non-null value is present, and {@code false} otherwise.
*/
public boolean isPresent() {
return (this.value != null);
}
/**
* Return {@code true} if the input value was present in the input but the value was {@code null},
* and {@code false} otherwise.
*/
public boolean isEmpty() {
return !this.omitted && this.value == null;
}
/**
* Return {@code true} if the input value was omitted altogether from the
* input, and {@code false} if it was provided, but possibly set to the
* {@literal "null"} literal.
*/
public boolean isOmitted() {
return this.omitted;
}
/**
* Return the contained value, or {@code null}.
*/
@Nullable
public T value() {
return this.value;
}
/**
* Return the contained value as a nullable {@link Optional}.
*/
public Optional<T> asOptional() {
return Optional.ofNullable(this.value);
}
/**
* If a value is present, performs the given action with the value, otherwise does nothing.
* @param action the action to be performed, if a value is present
*/
public void ifPresent(Consumer<? super T> action) {
Assert.notNull(action, "Action is required");
if (this.value != null) {
action.accept(this.value);
}
}
@Override
public boolean equals(Object other) {
// This covers EMPTY and OMITTED constant
if (this == other) {
return true;
}
if (!(other instanceof FieldValue<?> otherValue)) {
return false;
}
return ObjectUtils.nullSafeEquals(this.value, otherValue.value);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(this.value);
result = 31 * result + Boolean.hashCode(this.omitted);
return result;
}
@Override
public String toString() {
if (this.isOmitted()) {
return "FieldValue{omitted}";
}
return "FieldValue{value=" + this.value + "'}'";
}
/**
* Static factory method for an argument value that was provided, even if
* it was set to {@literal "null}.
* @param <T> the type of value
* @param value the value to hold in the instance
*/
@SuppressWarnings("unchecked")
public static <T> FieldValue<T> ofNullable(@Nullable T value) {
return (value != null) ? new FieldValue<>(value, false) : (FieldValue<T>) EMPTY;
}
/**
* Static factory method for an argument value that was omitted.
* @param <T> the type of value
*/
@SuppressWarnings("unchecked")
public static <T> FieldValue<T> omitted() {
return (FieldValue<T>) OMITTED;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,22 +23,16 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.MediaTypes;
import org.springframework.graphql.client.GraphQlClientInterceptor.Chain;
import org.springframework.graphql.client.GraphQlClientInterceptor.SubscriptionChain;
import org.springframework.graphql.client.json.GraphQlModule;
import org.springframework.graphql.support.CachingDocumentSource;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -241,15 +235,12 @@ public abstract class AbstractGraphQlClientBuilder<B extends AbstractGraphQlClie
protected static class DefaultJackson2Codecs {
private static final ObjectMapper JSON_MAPPER = Jackson2ObjectMapperBuilder.json()
.modulesToInstall(new GraphQlModule()).build();
static Encoder<?> encoder() {
return new Jackson2JsonEncoder(JSON_MAPPER, MediaType.APPLICATION_JSON);
return new Jackson2JsonEncoder();
}
static Decoder<?> decoder() {
return new Jackson2JsonDecoder(JSON_MAPPER, MediaType.APPLICATION_JSON, MediaTypes.APPLICATION_GRAPHQL_RESPONSE);
return new Jackson2JsonDecoder();
}
}

View File

@@ -22,7 +22,6 @@ import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
@@ -31,12 +30,10 @@ import org.springframework.core.codec.Encoder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.graphql.GraphQlResponse;
import org.springframework.graphql.client.SyncGraphQlClientInterceptor.Chain;
import org.springframework.graphql.client.json.GraphQlModule;
import org.springframework.graphql.support.CachingDocumentSource;
import org.springframework.graphql.support.DocumentSource;
import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -194,9 +191,7 @@ public abstract class AbstractGraphQlClientSyncBuilder<B extends AbstractGraphQl
private static final class DefaultJacksonConverter {
static HttpMessageConverter<Object> initialize() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
.modulesToInstall(new GraphQlModule()).build();
return new MappingJackson2HttpMessageConverter(objectMapper);
return new MappingJackson2HttpMessageConverter();
}
}

View File

@@ -1,92 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import java.io.Serial;
import java.io.Serializable;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
import com.fasterxml.jackson.databind.deser.std.ReferenceTypeDeserializer;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.ser.std.ReferenceTypeSerializer;
import org.springframework.graphql.FieldValue;
import org.springframework.lang.Nullable;
/**
* {@link ReferenceTypeSerializer} that deserializes JSON values as {@link FieldValue}:
* <ul>
* <li>a {@link FieldValue#isEmpty() non empty FieldValue} when the JSON key is present and its value is not {@literal null}.
* <li>an {@link FieldValue#isEmpty() empty FieldValue} when the JSON key is present and its value is {@literal null}.
* <li>an {@link FieldValue#isOmitted() ommitted FieldValue} when the JSON key is not present.
* </ul>
* @author James Bodkin
*/
class FieldValueDeserializer extends ReferenceTypeDeserializer<FieldValue<?>> {
@Serial
private static final long serialVersionUID = 1L;
FieldValueDeserializer(final JavaType fullType, @Nullable final ValueInstantiator vi,
final TypeDeserializer typeDeser, final JsonDeserializer<?> deser) {
super(fullType, vi, typeDeser, deser);
}
@Override
protected ReferenceTypeDeserializer<FieldValue<?>> withResolved(final TypeDeserializer typeDeser,
final JsonDeserializer<?> valueDeser) {
return new FieldValueDeserializer(_fullType, _valueInstantiator, typeDeser, valueDeser);
}
@Override
public FieldValue<? extends Serializable> getNullValue(final DeserializationContext ctxt) throws JsonMappingException {
return FieldValue.ofNullable((Serializable) _valueDeserializer.getNullValue(ctxt));
}
@Override
public Object getEmptyValue(final DeserializationContext ctxt) throws JsonMappingException {
return getNullValue(ctxt);
}
@Override
public Object getAbsentValue(final DeserializationContext ctxt) {
return FieldValue.omitted();
}
@Override
public FieldValue<?> referenceValue(final Object contents) {
return FieldValue.ofNullable((Serializable) contents);
}
@Nullable
@Override
public Object getReferenced(final FieldValue<?> value) {
return value.value();
}
@Override
public FieldValue<?> updateReference(final FieldValue<?> value, final Object contents) {
return FieldValue.ofNullable((Serializable) contents);
}
}

View File

@@ -1,89 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import java.io.Serial;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.std.ReferenceTypeSerializer;
import com.fasterxml.jackson.databind.type.ReferenceType;
import com.fasterxml.jackson.databind.util.NameTransformer;
import org.springframework.graphql.FieldValue;
import org.springframework.lang.Nullable;
/**
* {@link ReferenceTypeSerializer} that serializes {@link FieldValue} values as:
* <ul>
* <li>the embedded value if it is present and not {@literal null}.
* <li>{@literal null} if the embedded value is present and {@literal null}.
* <li>an empty value if the embedded value is not present.
* </ul>
* @author James Bodkin
*/
class FieldValueSerializer extends ReferenceTypeSerializer<FieldValue<?>> {
@Serial
private static final long serialVersionUID = 1L;
FieldValueSerializer(final ReferenceType fullType, final boolean staticTyping,
@Nullable final TypeSerializer vts, final JsonSerializer<Object> ser) {
super(fullType, staticTyping, vts, ser);
}
protected FieldValueSerializer(final FieldValueSerializer base, final BeanProperty property,
final TypeSerializer vts, final JsonSerializer<?> valueSer,
final NameTransformer unwrapper, final Object suppressableValue,
final boolean suppressNulls) {
super(base, property, vts, valueSer, unwrapper, suppressableValue, suppressNulls);
}
@Override
protected ReferenceTypeSerializer<FieldValue<?>> withResolved(final BeanProperty prop, final TypeSerializer vts,
final JsonSerializer<?> valueSer, final NameTransformer unwrapper) {
return new FieldValueSerializer(this, prop, vts, valueSer, unwrapper, _suppressableValue, _suppressNulls);
}
@Override
public ReferenceTypeSerializer<FieldValue<?>> withContentInclusion(final Object suppressableValue, final boolean suppressNulls) {
return new FieldValueSerializer(this, _property, _valueTypeSerializer,
_valueSerializer, _unwrapper, suppressableValue, suppressNulls);
}
@Override
protected boolean _isValuePresent(final FieldValue<?> value) {
return !value.isOmitted();
}
@Nullable
@Override
protected Object _getReferenced(final FieldValue<?> value) {
return value.value();
}
@Nullable
@Override
protected Object _getReferencedIfPresent(final FieldValue<?> value) {
return value.value();
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import java.lang.reflect.Type;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.ReferenceType;
import com.fasterxml.jackson.databind.type.TypeBindings;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.type.TypeModifier;
import org.springframework.graphql.FieldValue;
/**
* {@link TypeModifier} that upgrades {@link FieldValue} types to {@link ReferenceType}.
*
* @author James Bodkin
*/
class FieldValueTypeModifier extends TypeModifier {
@Override
public JavaType modifyType(final JavaType type, final Type jdkType, final TypeBindings context, final TypeFactory typeFactory) {
Class<?> raw = type.getRawClass();
if (!type.isReferenceType() && !type.isContainerType() && raw == FieldValue.class) {
JavaType refType = type.containedTypeOrUnknown(0);
return ReferenceType.upgradeFrom(type, refType);
}
else {
return type;
}
}
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.Deserializers;
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
import com.fasterxml.jackson.databind.type.ReferenceType;
import org.springframework.graphql.FieldValue;
import org.springframework.lang.Nullable;
class GraphQlDeserializers extends Deserializers.Base {
@Nullable
@Override
public JsonDeserializer<?> findReferenceDeserializer(final ReferenceType refType, final DeserializationConfig config,
final BeanDescription beanDesc, final TypeDeserializer contentTypeDeserializer,
final JsonDeserializer<?> contentDeserializer) {
if (refType.hasRawClass(FieldValue.class)) {
return new FieldValueDeserializer(refType, null, contentTypeDeserializer, contentDeserializer);
}
else {
return null;
}
}
}

View File

@@ -1,58 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.type.ReferenceType;
import org.springframework.graphql.FieldValue;
/**
* {@link Module Jackson module} for JSON support in GraphQL clients.
* <p>This module ships with the following features:
* <ul>
* <li>Manage {@link FieldValue} types as {@link ReferenceType}, similar to {@link java.util.Optional}
* <li>Serializing and Deserializing values contained in {@link FieldValue} reference types
* </ul>
* @author James Bodkin
* @since 1.4.0
*/
public class GraphQlModule extends Module {
@Override
public String getModuleName() {
return GraphQlModule.class.getName();
}
@Override
public Version version() {
return Version.unknownVersion();
}
@Override
public void setupModule(final SetupContext context) {
context.addSerializers(new GraphQlSerializers());
context.addDeserializers(new GraphQlDeserializers());
context.addTypeModifier(new FieldValueTypeModifier());
context.configOverride(FieldValue.class)
.setInclude(JsonInclude.Value.empty().withValueInclusion(JsonInclude.Include.NON_ABSENT));
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.Serializers;
import com.fasterxml.jackson.databind.type.ReferenceType;
import org.springframework.graphql.FieldValue;
import org.springframework.lang.Nullable;
class GraphQlSerializers extends Serializers.Base {
@Nullable
@Override
public JsonSerializer<?> findReferenceSerializer(final SerializationConfig config, final ReferenceType refType,
final BeanDescription beanDesc, @Nullable final TypeSerializer contentTypeSerializer,
final JsonSerializer<Object> contentValueSerializer) {
Class<?> raw = refType.getRawClass();
if (FieldValue.class.isAssignableFrom(raw)) {
boolean staticTyping = contentTypeSerializer == null && config.isEnabled(MapperFeature.USE_STATIC_TYPING);
return new FieldValueSerializer(refType, staticTyping, contentTypeSerializer, contentValueSerializer);
}
else {
return null;
}
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This package contains JSON support for GraphQL clients.
*/
@NonNullApi
@NonNullFields
package org.springframework.graphql.client.json;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;

View File

@@ -18,8 +18,10 @@ package org.springframework.graphql.data;
import java.util.Optional;
import java.util.function.Consumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
@@ -44,9 +46,7 @@ import org.springframework.util.ObjectUtils;
* @author Rossen Stoyanchev
* @since 1.1.0
* @see <a href="http://spec.graphql.org/October2021/#sec-Non-Null.Nullable-vs-Optional">Nullable vs Optional</a>
* @deprecated since 1.4.0 in favor of {@link org.springframework.graphql.FieldValue}.
*/
@Deprecated(since = "1.4.0", forRemoval = true)
public final class ArgumentValue<T> {
private static final ArgumentValue<?> EMPTY = new ArgumentValue<>(null, false);
@@ -73,6 +73,15 @@ public final class ArgumentValue<T> {
return (this.value != null);
}
/**
* Return {@code true} if the input value was present in the input but the value was {@code null},
* and {@code false} otherwise.
* @since 1.4.0
*/
public boolean isEmpty() {
return !this.omitted && this.value == null;
}
/**
* Return {@code true} if the input value was omitted altogether from the
* input, and {@code false} if it was provided, but possibly set to the
@@ -97,6 +106,18 @@ public final class ArgumentValue<T> {
return Optional.ofNullable(this.value);
}
/**
* If a value is present, performs the given action with the value, otherwise does nothing.
* @param action the action to be performed, if a value is present
* @since 1.4.0
*/
public void ifPresent(Consumer<? super T> action) {
Assert.notNull(action, "Action is required");
if (this.value != null) {
action.accept(this.value);
}
}
@Override
public boolean equals(Object other) {
// This covers EMPTY and OMITTED constant

View File

@@ -40,7 +40,6 @@ import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper;
import org.springframework.graphql.FieldValue;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
@@ -68,15 +67,13 @@ import org.springframework.validation.FieldError;
*
* <p>The binder supports {@link Optional} as a wrapper around any Object or
* scalar value in the target Object structure. In addition, it also supports
* {@link org.springframework.graphql.FieldValue} as a wrapper that indicates
* whether a given input argument was omitted rather than set to the
* {@literal "null"} literal.
* {@link ArgumentValue} as a wrapper that indicates whether a given input
* argument was omitted rather than set to the {@literal "null"} literal.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @since 1.0.0
*/
@SuppressWarnings("removal")
public class GraphQlArgumentBinder {
@Nullable
@@ -118,7 +115,7 @@ public class GraphQlArgumentBinder {
* @param name the name of an argument, or {@code null} to use the full map
* @param targetType the type of Object to create
* @return the created Object, possibly wrapped in {@link Optional} or in
* {@link org.springframework.graphql.FieldValue}, or {@code null} if there is no value
* {@link ArgumentValue}, or {@code null} if there is no value
* @throws BindException containing one or more accumulated errors from
* matching and/or converting arguments to the target Object
*/
@@ -178,9 +175,8 @@ public class GraphQlArgumentBinder {
boolean isOptional = (targetClass == Optional.class);
boolean isArgumentValue = (targetClass == ArgumentValue.class);
boolean isFieldValue = (targetClass == FieldValue.class);
if (isOptional || isArgumentValue || isFieldValue) {
if (isOptional || isArgumentValue) {
targetType = targetType.getNested(2);
targetClass = targetType.resolve();
}
@@ -206,9 +202,6 @@ public class GraphQlArgumentBinder {
else if (isArgumentValue) {
value = (isOmitted ? ArgumentValue.omitted() : ArgumentValue.ofNullable(value));
}
else if (isFieldValue) {
value = (isOmitted ? FieldValue.omitted() : FieldValue.ofNullable(value));
}
return value;
}

View File

@@ -61,7 +61,6 @@ import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethod;
@@ -489,12 +488,10 @@ public class AnnotatedControllerConfigurer
}
@Override
@SuppressWarnings("removal")
public Map<String, ResolvableType> getArguments() {
Predicate<MethodParameter> argumentPredicate = (p) ->
(p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class ||
p.getParameterType() == FieldValue.class);
(p.getParameterAnnotation(Argument.class) != null || p.getParameterType() == ArgumentValue.class);
return Arrays.stream(this.mappingInfo.getHandlerMethod().getMethodParameters())
.filter(argumentPredicate)

View File

@@ -20,7 +20,6 @@ import graphql.schema.DataFetchingEnvironment;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
@@ -38,13 +37,12 @@ import org.springframework.validation.BindException;
* parameter type.
*
* <p>This resolver also supports wrapping the target object with
* {@link FieldValue} if the application
* wants to differentiate between an input argument that was set to
* {@code null} vs not provided at all.
* When this wrapper type is used, the annotation is optional,
* and the name of the argument is derived from the method parameter name.
* {@link ArgumentValue} if the application wants to differentiate between an
* input argument that was set to {@code null} vs not provided at all. When
* this wrapper type is used, the annotation is optional, and the name of the
* argument is derived from the method parameter name.
*
* <p>An {@link FieldValue} can also be nested within the object structure
* <p>An {@link ArgumentValue} can also be nested within the object structure
* of an {@link Argument @Argument}-annotated method parameter.
*
* @author Rossen Stoyanchev
@@ -52,7 +50,6 @@ import org.springframework.validation.BindException;
* @since 1.0.0
* @see org.springframework.graphql.data.method.annotation.support.ArgumentsMethodArgumentResolver
*/
@SuppressWarnings("removal")
public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentResolver {
private final GraphQlArgumentBinder argumentBinder;
@@ -75,8 +72,7 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.getParameterAnnotation(Argument.class) != null ||
parameter.getParameterType() == ArgumentValue.class ||
parameter.getParameterType() == FieldValue.class);
parameter.getParameterType() == ArgumentValue.class);
}
@Override
@@ -107,10 +103,9 @@ public class ArgumentMethodArgumentResolver implements HandlerMethodArgumentReso
return argument.name();
}
}
else if (parameter.getParameterType() != ArgumentValue.class &&
parameter.getParameterType() != FieldValue.class) {
else if (parameter.getParameterType() != ArgumentValue.class) {
throw new IllegalStateException(
"Expected either @Argument or a method parameter of type FieldValue");
"Expected either @Argument or a method parameter of type ArgumentValue");
}
String parameterName = parameter.getParameterName();

View File

@@ -30,7 +30,6 @@ import org.springframework.graphql.data.ArgumentValue;
* @since 1.2.2
*/
@UnwrapByDefault
@SuppressWarnings("removal")
public final class ArgumentValueValueExtractor implements ValueExtractor<ArgumentValue<@ExtractedValue ?>> {
@Override

View File

@@ -1,42 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.data.method.annotation.support;
import jakarta.validation.valueextraction.ExtractedValue;
import jakarta.validation.valueextraction.UnwrapByDefault;
import jakarta.validation.valueextraction.ValueExtractor;
import org.springframework.graphql.FieldValue;
/**
* {@link ValueExtractor} that enables {@code @Valid} with {@link FieldValue},
* and helps to extract the value from it.
*
* @author Rossen Stoyanchev
* @since 1.4.0
*/
@UnwrapByDefault
public final class FieldValueValueExtractor implements ValueExtractor<FieldValue<@ExtractedValue ?>> {
@Override
public void extractValues(FieldValue<?> fieldValue, ValueReceiver receiver) {
if (!fieldValue.isOmitted()) {
receiver.value(null, fieldValue.value());
}
}
}

View File

@@ -27,7 +27,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.web.ProjectedPayload;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Argument;
@@ -98,16 +97,13 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu
return (type.isInterface() && AnnotatedElementUtils.findMergedAnnotation(type, ProjectedPayload.class) != null);
}
@SuppressWarnings("removal")
private static Class<?> getTargetType(MethodParameter parameter) {
Class<?> type = parameter.getParameterType();
return (type.equals(Optional.class) || type.equals(ArgumentValue.class) ||
type.equals(FieldValue.class)) ?
return (type.equals(Optional.class) || type.equals(ArgumentValue.class)) ?
parameter.nested().getNestedParameterType() : parameter.getParameterType();
}
@Override
@SuppressWarnings("removal")
public Object resolveArgument(MethodParameter parameter, DataFetchingEnvironment environment) throws Exception {
String name = (parameter.hasParameterAnnotation(Argument.class) ?
@@ -116,9 +112,8 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu
Class<?> targetType = parameter.getParameterType();
boolean isOptional = (targetType == Optional.class);
boolean isArgumentValue = (targetType == ArgumentValue.class);
boolean isFieldValue = (targetType == FieldValue.class);
if (isOptional || isArgumentValue || isFieldValue) {
if (isOptional || isArgumentValue) {
targetType = parameter.nested().getNestedParameterType();
}
@@ -133,10 +128,6 @@ public class ProjectedPayloadMethodArgumentResolver implements HandlerMethodArgu
return (name != null && arguments.containsKey(name)) ?
ArgumentValue.ofNullable(value) : ArgumentValue.omitted();
}
else if (isFieldValue) {
return (name != null && arguments.containsKey(name)) ?
FieldValue.ofNullable(value) : FieldValue.omitted();
}
else {
return value;
}

View File

@@ -43,7 +43,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.projection.TargetAware;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
@@ -246,11 +245,9 @@ class SchemaMappingBeanFactoryInitializationAotProcessor implements BeanFactoryI
}
@Override
@SuppressWarnings("removal")
public void apply(RuntimeHints runtimeHints) {
Type parameterType = this.methodParameter.getGenericParameterType();
if (ArgumentValue.class.isAssignableFrom(this.methodParameter.getParameterType()) ||
FieldValue.class.isAssignableFrom(this.methodParameter.getParameterType())) {
if (ArgumentValue.class.isAssignableFrom(this.methodParameter.getParameterType())) {
parameterType = this.methodParameter.nested().getNestedGenericParameterType();
}
bindingRegistrar.registerReflectionHints(runtimeHints.reflection(), parameterType);

View File

@@ -1,2 +1 @@
org.springframework.graphql.data.method.annotation.support.FieldValueValueExtractor
org.springframework.graphql.data.method.annotation.support.ArgumentValueValueExtractor

View File

@@ -31,7 +31,6 @@ import graphql.validation.ValidationErrorType;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.GraphQlRequest;
import org.springframework.graphql.support.DefaultGraphQlRequest;
@@ -59,45 +58,6 @@ class GraphQlClientTests extends GraphQlClientTestSupport {
assertThat(movieCharacter).isEqualTo(MovieCharacter.create("Luke Skywalker"));
}
@Test
void retrieveEntityFieldValuePresent() {
String document = "mockRequest1";
getGraphQlService().setDataAsJson(document, "{\"current\": {\"id\":\"spring-graphql\", \"name\":\"Spring for GraphQL\"}}");
Project currentProject = graphQlClient().document(document)
.retrieve("current").toEntity(Project.class)
.block(TIMEOUT);
assertThat(currentProject).isEqualTo(new Project("spring-graphql", FieldValue.ofNullable("Spring for GraphQL")));
}
@Test
void retrieveEntityOmittedField() {
String document = "mockRequest1";
getGraphQlService().setDataAsJson(document, "{\"current\": {\"id\":\"spring-graphql\"}}");
Project currentProject = graphQlClient().document(document)
.retrieve("current").toEntity(Project.class)
.block(TIMEOUT);
assertThat(currentProject).isEqualTo(new Project("spring-graphql", FieldValue.omitted()));
}
@Test
void retrieveEntityNullField() {
String document = "mockRequest1";
getGraphQlService().setDataAsJson(document, "{\"current\": {\"id\":\"spring-graphql\", \"name\": null}}");
Project currentProject = graphQlClient().document(document)
.retrieve("current").toEntity(Project.class)
.block(TIMEOUT);
assertThat(currentProject).isEqualTo(new Project("spring-graphql", FieldValue.ofNullable(null)));
}
@Test
void retrieveEntityList() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2025 the original author or authors.
* Copyright 2020-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,28 +17,16 @@
package org.springframework.graphql.client;
import java.util.Map;
import java.util.stream.Stream;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.graphql.Book;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.MediaTypes;
import org.springframework.graphql.MockWebServerExtension;
import org.springframework.graphql.client.json.GraphQlModule;
import org.springframework.http.MediaType;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
@@ -51,49 +39,6 @@ import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(MockWebServerExtension.class)
class HttpGraphQlTransportIntegrationTests {
@ParameterizedTest
@MethodSource("fieldValues")
void shouldSerializeFieldValues(ProjectInput projectInput, String variable, MockWebServer server) throws Exception {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(new GraphQlModule()).build();
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON);
WebClient webClient = WebClient.builder()
.baseUrl(server.url("/graphql").toString())
.codecs(codecs -> codecs.defaultCodecs().jackson2JsonEncoder(jsonEncoder))
.build();
HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient);
server.enqueue(new MockResponse().addHeader("Content-Type", MediaTypes.APPLICATION_GRAPHQL_RESPONSE)
.setBody("""
{
"data": {
"createProject": {
"id": "spring-graphql"
}
}
}
"""));
graphQlClient.document("""
mutation createProject($project: ProjectInput!) {
createProject($project: $project) {
id
}
}
""")
.variables(Map.of("project", projectInput))
.executeSync();
assertThat(server.takeRequest().getBody().readUtf8()).contains("\"variables\":{\"project\":" + variable + "}");
}
static Stream<Arguments> fieldValues() {
return Stream.of(
Arguments.arguments(new ProjectInput("spring-graphql", FieldValue.omitted()), "{\"id\":\"spring-graphql\"}"),
Arguments.arguments(new ProjectInput("spring-graphql", FieldValue.ofNullable(null)), "{\"id\":\"spring-graphql\",\"name\":null}"),
Arguments.arguments(new ProjectInput("spring-graphql", FieldValue.ofNullable("Spring for GraphQL")), "{\"id\":\"spring-graphql\",\"name\":\"Spring for GraphQL\"}")
);
}
@Test
void shouldStreamSubscriptionResultsOverSse(MockWebServer server) {
WebClient webClient = WebClient.create(server.url("/graphql").toString());
@@ -150,8 +95,4 @@ class HttpGraphQlTransportIntegrationTests {
}
public record ProjectInput(String id, FieldValue<String> name) {
}
}

View File

@@ -1,23 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client;
import org.springframework.graphql.FieldValue;
public record Project(String id, FieldValue<String> name) {
}

View File

@@ -1,103 +0,0 @@
/*
* Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.client.json;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.graphql.FieldValue;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link org.springframework.graphql.FieldValue} support in {@link GraphQlModule}.
*/
class FieldValueJsonTests {
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new GraphQlModule());
@Nested
class DeserializationTests {
@Test
void valueIsOmittedWhenJsonKeyMissing() throws Exception {
Library library = objectMapper.readValue("{}", Library.class);
assertThat(library.name()).isNotNull()
.satisfies(name -> assertThat(name.isOmitted()).isTrue());
}
@Test
void valueIsEmptyWhenJsonValueIsNull() throws Exception {
Library library = objectMapper.readValue("{\"name\":null}", Library.class);
assertThat(library.name()).isNotNull()
.satisfies(name -> assertThat(name.isEmpty()).isTrue());
}
@Test
void valueIsPresentWhenJsonValueIsNotNull() throws Exception {
Library library = objectMapper.readValue("{\"name\":\"The Library\"}", Library.class);
assertThat(library.name()).isNotNull()
.satisfies(
name -> assertThat(name.isPresent()).isTrue(),
name -> assertThat(name.value()).isEqualTo("The Library")
);
}
}
@Nested
class SerializationTests {
@Test
void skipJsonAttributeWhenValueOmitted() throws Exception {
Library library = new Library(FieldValue.omitted());
assertThat(objectMapper.writeValueAsString(library)).isEqualTo("{}");
}
@Test
void nullJsonValueWhenValueIsNull() throws Exception {
Library library = new Library(FieldValue.ofNullable(null));
assertThat(objectMapper.writeValueAsString(library)).isEqualTo("{\"name\":null}");
}
@Test
void emptyJsonValueWhenValueIsEmpty() throws Exception {
Library library = new Library(FieldValue.ofNullable(""));
assertThat(objectMapper.writeValueAsString(library)).isEqualTo("{\"name\":\"\"}");
}
@Test
void jsonValueWhenValueIsPresent() throws Exception {
Library library = new Library(FieldValue.ofNullable("The Library"));
assertThat(objectMapper.writeValueAsString(library)).isEqualTo("{\"name\":\"The Library\"}");
}
}
public record Library(FieldValue<String> name) {
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.graphql;
package org.springframework.graphql.data;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -23,14 +23,14 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link FieldValue}.
* Tests for {@link ArgumentValue}.
* @author Brian Clozel
*/
class FieldValueTests {
class ArgumentValueTests {
@Test
void existingValueShouldBePresent() {
FieldValue<String> message = FieldValue.ofNullable("hello");
ArgumentValue<String> message = ArgumentValue.ofNullable("hello");
assertThat(message.isOmitted()).isFalse();
assertThat(message.isPresent()).isTrue();
assertThat(message.isEmpty()).isFalse();
@@ -38,7 +38,7 @@ class FieldValueTests {
@Test
void nullValueShouldBePresent() {
FieldValue<String> message = FieldValue.ofNullable(null);
ArgumentValue<String> message = ArgumentValue.ofNullable(null);
assertThat(message.isOmitted()).isFalse();
assertThat(message.isPresent()).isFalse();
assertThat(message.isEmpty()).isTrue();
@@ -46,7 +46,7 @@ class FieldValueTests {
@Test
void noValueShouldBeOmitted() {
FieldValue<String> message = FieldValue.omitted();
ArgumentValue<String> message = ArgumentValue.omitted();
assertThat(message.isOmitted()).isTrue();
assertThat(message.isPresent()).isFalse();
assertThat(message.isEmpty()).isFalse();
@@ -54,29 +54,29 @@ class FieldValueTests {
@Test
void asOptionalShouldMapOmitted() {
assertThat(FieldValue.omitted().asOptional()).isEmpty();
assertThat(FieldValue.ofNullable(null).asOptional()).isEmpty();
assertThat(FieldValue.ofNullable("hello").asOptional()).isPresent();
assertThat(ArgumentValue.omitted().asOptional()).isEmpty();
assertThat(ArgumentValue.ofNullable(null).asOptional()).isEmpty();
assertThat(ArgumentValue.ofNullable("hello").asOptional()).isPresent();
}
@Test
void ifPresentShouldExecuteWhenValue() {
AtomicBoolean called = new AtomicBoolean();
FieldValue.ofNullable("hello").ifPresent(value -> called.set(true));
ArgumentValue.ofNullable("hello").ifPresent(value -> called.set(true));
assertThat(called.get()).isTrue();
}
@Test
void ifPresentShouldSkipWhenNull() {
AtomicBoolean called = new AtomicBoolean();
FieldValue.ofNullable(null).ifPresent(value -> called.set(true));
ArgumentValue.ofNullable(null).ifPresent(value -> called.set(true));
assertThat(called.get()).isFalse();
}
@Test
void ifPresentShouldSkipWhenOmitted() {
AtomicBoolean called = new AtomicBoolean();
FieldValue.omitted().ifPresent(value -> called.set(true));
ArgumentValue.omitted().ifPresent(value -> called.set(true));
assertThat(called.get()).isFalse();
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2025 the original author or authors.
* Copyright 2020-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +38,6 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.Book;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.lang.Nullable;
import org.springframework.validation.BindException;
@@ -54,7 +53,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* @author Brian Clozel
* @author Rossen Stoyanchev
*/
@SuppressWarnings("removal")
class GraphQlArgumentBinderTests {
private final ObjectMapper mapper = new ObjectMapper();
@@ -250,30 +248,6 @@ class GraphQlArgumentBinderTests {
assertThat(itemBean.getName().isPresent()).isFalse();
}
@Test
void primaryConstructorWithOptionalFieldBeanArgument() throws Exception {
ResolvableType targetType =
ResolvableType.forClass(PrimaryConstructorOptionalFieldItemBean.class);
Object result = bind(
"{\"item\":{\"name\":\"Item name\",\"age\":\"30\"},\"name\":\"Hello\"}", targetType);
assertThat(result).isInstanceOf(PrimaryConstructorOptionalFieldItemBean.class).isNotNull();
PrimaryConstructorOptionalFieldItemBean itemBean = (PrimaryConstructorOptionalFieldItemBean) result;
assertThat(itemBean.getItem().value().getName()).isEqualTo("Item name");
assertThat(itemBean.getItem().value().getAge()).isEqualTo(30);
assertThat(itemBean.getName().value()).isEqualTo("Hello");
result = bind("{\"key\":{}}", targetType);
itemBean = (PrimaryConstructorOptionalFieldItemBean) result;
assertThat(itemBean).isNotNull();
assertThat(itemBean.getItem().isOmitted()).isTrue();
assertThat(itemBean.getName().isPresent()).isFalse();
}
@Test
void primaryConstructorWithNestedBeanList() throws Exception {
@@ -596,26 +570,6 @@ class GraphQlArgumentBinderTests {
}
}
static class PrimaryConstructorOptionalFieldItemBean {
private final FieldValue<String> name;
private final FieldValue<Item> item;
public PrimaryConstructorOptionalFieldItemBean(FieldValue<String> name, FieldValue<Item> item) {
this.name = name;
this.item = item;
}
public FieldValue<String> getName() {
return this.name;
}
public FieldValue<Item> getItem() {
return item;
}
}
@SuppressWarnings("unused")
static class NoPrimaryConstructorBean {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2025 the original author or authors.
* Copyright 2020-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.Book;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
@@ -54,9 +53,6 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
param = methodParam(BookController.class, "addBook", ArgumentValue.class);
assertThat(this.resolver.supportsParameter(param)).isTrue();
param = methodParam(BookController.class, "addBook", FieldValue.class);
assertThat(this.resolver.supportsParameter(param)).isTrue();
param = methodParam(BookController.class, "rawArgumentValue", Map.class);
assertThat(this.resolver.supportsParameter(param)).isTrue();
@@ -85,7 +81,7 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
}
@Test
void shouldResolveJavaBeanArgumentWithArgumentWrapper() throws Exception {
void shouldResolveJavaBeanArgumentWithWrapper() throws Exception {
Object result = this.resolver.resolveArgument(
methodParam(BookController.class, "addBook", ArgumentValue.class),
environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }"));
@@ -98,20 +94,6 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
.hasFieldOrPropertyWithValue("authorId", 42L);
}
@Test
void shouldResolveJavaBeanArgumentWithFieldWrapper() throws Exception {
Object result = this.resolver.resolveArgument(
methodParam(BookController.class, "addBook", FieldValue.class),
environment("{\"bookInput\": { \"name\": \"test name\", \"authorId\": 42} }"));
assertThat(result)
.isNotNull()
.isInstanceOf(FieldValue.class)
.extracting(value -> ((FieldValue<?>) value).value())
.hasFieldOrPropertyWithValue("name", "test name")
.hasFieldOrPropertyWithValue("authorId", 42L);
}
@Test
void shouldResolveListOfJavaBeansArgument() throws Exception {
Object result = this.resolver.resolveArgument(
@@ -170,11 +152,6 @@ class ArgumentMethodArgumentResolverTests extends ArgumentResolverTestSupport {
return null;
}
@MutationMapping
public Book addBook(FieldValue<BookInput> bookInput) {
return null;
}
@MutationMapping
public List<Book> addBooks(@Argument List<Book> books) {
return null;

View File

@@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.Book;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.GraphQlArgumentBinder;
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
import org.springframework.graphql.data.method.annotation.Arguments;
@@ -109,15 +109,15 @@ class ArgumentsMethodArgumentResolverTests extends ArgumentResolverTestSupport {
@SuppressWarnings({"NotNullFieldNotInitialized", "unused"})
static class BookInput {
FieldValue<String> name;
ArgumentValue<String> name;
Long authorId;
public FieldValue<String> getName() {
public ArgumentValue<String> getName() {
return this.name;
}
public void setName(FieldValue<String> name) {
public void setName(ArgumentValue<String> name) {
this.name = name;
}

View File

@@ -26,7 +26,7 @@ import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.data.web.ProjectedPayload;
import org.springframework.graphql.Book;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
@@ -54,7 +54,7 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu
testSupports("projection", BookProjection.class, true);
testSupports("optionalProjection", Optional.class, true);
testSupports("optionalString", Optional.class, false);
testSupports("fieldValueProjection", FieldValue.class, true);
testSupports("argumentValueProjection", ArgumentValue.class, true);
}
void testSupports(String methodName, Class<?> methodParamType, boolean supported) {
@@ -86,39 +86,39 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu
}
@Test
void fieldValuePresent() throws Exception {
void argumentValuePresent() throws Exception {
Object result = this.resolver.resolveArgument(
methodParam(BookController.class, "fieldValueProjection", FieldValue.class),
methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class),
environment("{ \"where\" : { \"author\" : \"Orwell\" }}"));
assertThat(result).isNotNull().isInstanceOf(FieldValue.class);
BookProjection book = ((FieldValue<BookProjection>) result).value();
assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class);
BookProjection book = ((ArgumentValue<BookProjection>) result).value();
assertThat(book.getAuthor()).isEqualTo("Orwell");
}
@Test
void fieldValueSetToNull() throws Exception {
void argumentValueSetToNull() throws Exception {
Object result = this.resolver.resolveArgument(
methodParam(BookController.class, "fieldValueProjection", FieldValue.class),
methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class),
environment("{ \"where\" : null}"));
assertThat(result).isNotNull().isInstanceOf(FieldValue.class);
FieldValue<BookProjection> value = ((FieldValue<BookProjection>) result);
assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class);
ArgumentValue<BookProjection> value = ((ArgumentValue<BookProjection>) result);
assertThat(value.isPresent()).isFalse();
assertThat(value.isOmitted()).isFalse();
}
@Test
void fieldValueIsOmitted() throws Exception {
void argumentValueIsOmitted() throws Exception {
Object result = this.resolver.resolveArgument(
methodParam(BookController.class, "fieldValueProjection", FieldValue.class),
methodParam(BookController.class, "argumentValueProjection", ArgumentValue.class),
environment("{}"));
assertThat(result).isNotNull().isInstanceOf(FieldValue.class);
FieldValue<BookProjection> value = ((FieldValue<BookProjection>) result);
assertThat(result).isNotNull().isInstanceOf(ArgumentValue.class);
ArgumentValue<BookProjection> value = ((ArgumentValue<BookProjection>) result);
assertThat(value.isPresent()).isFalse();
assertThat(value.isOmitted()).isTrue();
}
@@ -153,7 +153,7 @@ class ProjectedPayloadMethodArgumentResolverTests extends ArgumentResolverTestSu
}
@QueryMapping
public List<Book> fieldValueProjection(@Argument(name = "where") FieldValue<BookProjection> projection) {
public List<Book> argumentValueProjection(@Argument(name = "where") ArgumentValue<BookProjection> projection) {
return null;
}

View File

@@ -58,7 +58,7 @@ import org.springframework.data.projection.TargetAware;
import org.springframework.data.web.ProjectedPayload;
import org.springframework.graphql.Author;
import org.springframework.graphql.Book;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.federation.EntityMapping;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.BatchMapping;
@@ -120,12 +120,12 @@ class SchemaMappingBeanFactoryInitializationAotProcessorTests {
}
@Test
void registerBindingReflectionOnFieldValue() {
processBeanClasses(FieldValueController.class);
assertThatIntrospectionOnMethodsHintRegisteredForType(FieldValueController.class);
assertThatInvocationHintRegisteredForMethods(FieldValueController.class, "addBook");
void registerBindingReflectionOnArgumentValue() {
processBeanClasses(ArgumentValueController.class);
assertThatIntrospectionOnMethodsHintRegisteredForType(ArgumentValueController.class);
assertThatInvocationHintRegisteredForMethods(ArgumentValueController.class, "addBook");
assertThatHintsForJavaBeanBindingRegisteredForTypes(Book.class, BookInput.class);
assertThatHintsAreNotRegisteredForTypes(FieldValue.class);
assertThatHintsAreNotRegisteredForTypes(ArgumentValue.class);
}
@Test
@@ -173,9 +173,9 @@ class SchemaMappingBeanFactoryInitializationAotProcessorTests {
}
@Controller
static class FieldValueController {
static class ArgumentValueController {
@MutationMapping
public Book addBook(FieldValue<BookInput> bookInput) {
public Book addBook(ArgumentValue<BookInput> bookInput) {
return null;
}

View File

@@ -36,7 +36,7 @@ import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.graphql.FieldValue;
import org.springframework.graphql.data.ArgumentValue;
import org.springframework.graphql.data.method.HandlerMethod;
import org.springframework.validation.annotation.Validated;
@@ -71,20 +71,20 @@ class ValidationHelperTests {
BiConsumer<Object, Object[]> validator2 = validateFunction(MyBean.class, "myValidatedParameterMethod");
assertViolation(() -> validator2.accept(bean, new Object[] {new ConstrainedInput(100)}), "integerValue");
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidFieldValue");
assertViolation(() -> validator3.accept(bean, new Object[] {FieldValue.ofNullable("")}), "myValidFieldValue.arg0");
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidArgumentValue");
assertViolation(() -> validator3.accept(bean, new Object[] {ArgumentValue.ofNullable("")}), "myValidArgumentValue.arg0");
// Validate that an explicit null value is validated.
assertViolation(() -> validator3.accept(bean, new Object[] {FieldValue.ofNullable(null)}), "myValidFieldValue.arg0");
assertViolation(() -> validator3.accept(bean, new Object[] {ArgumentValue.ofNullable(null)}), "myValidArgumentValue.arg0");
}
@Test
void shouldNotRaiseValidationErrorForOmittedFieldValue() {
void shouldNotRaiseValidationErrorForOmittedArgumentValue() {
MyBean bean = new MyBean();
// Validate that an omitted value is allowed.
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidFieldValue");
validator3.accept(bean, new Object[] {FieldValue.omitted()});
BiConsumer<Object, Object[]> validator3 = validateFunction(MyBean.class, "myValidArgumentValue");
validator3.accept(bean, new Object[] {ArgumentValue.omitted()});
}
@Test
@@ -173,7 +173,7 @@ class ValidationHelperTests {
return null;
}
public Object myValidFieldValue(@Valid FieldValue<@NotBlank String> arg0) {
public Object myValidArgumentValue(@Valid ArgumentValue<@NotBlank String> arg0) {
return null;
}
}