Skip to content

Commit 3c30376

Browse files
committed
Refine null handling in GraphQlClient for retrieve
If a field is null but without errors, i.e. declared optional in the schema, it is more natural for toEntity to complete empty instead of raising a FieldAccessException. This aligns better with the execute method where handling the field directly allows treating a null but valid field as optional. See gh-10
1 parent b2b1e2d commit 3c30376

File tree

4 files changed

+51
-28
lines changed

4 files changed

+51
-28
lines changed

spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java

+30-13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.graphql.client;
1717

18+
import java.util.Collections;
1819
import java.util.LinkedHashMap;
1920
import java.util.List;
2021
import java.util.Map;
@@ -197,13 +198,17 @@ protected RetrieveSpecSupport(String path) {
197198
this.path = path;
198199
}
199200

200-
protected ResponseField getField(ClientGraphQlResponse response) {
201+
/**
202+
* Return the field if valid, possibly {@code null}.
203+
* @throws FieldAccessException if the response or field is not valid
204+
*/
205+
@Nullable
206+
protected ResponseField getValidField(ClientGraphQlResponse response) {
201207
ResponseField field = response.field(this.path);
202-
if (!field.hasValue() || !field.getErrors().isEmpty()) {
203-
GraphQlRequest request = response.getRequest();
204-
throw new FieldAccessException(request, response, field);
208+
if (!response.isValid() || field.getError() != null) {
209+
throw new FieldAccessException(response.getRequest(), response, field);
205210
}
206-
return field;
211+
return (field.hasValue() ? field : null);
207212
}
208213

209214
}
@@ -220,22 +225,28 @@ private static class DefaultRetrieveSpec extends RetrieveSpecSupport implements
220225

221226
@Override
222227
public <D> Mono<D> toEntity(Class<D> entityType) {
223-
return this.responseMono.map(this::getField).map(field -> field.toEntity(entityType));
228+
return this.responseMono.mapNotNull(this::getValidField).map(field -> field.toEntity(entityType));
224229
}
225230

226231
@Override
227232
public <D> Mono<D> toEntity(ParameterizedTypeReference<D> entityType) {
228-
return this.responseMono.map(this::getField).map(field -> field.toEntity(entityType));
233+
return this.responseMono.mapNotNull(this::getValidField).map(field -> field.toEntity(entityType));
229234
}
230235

231236
@Override
232237
public <D> Mono<List<D>> toEntityList(Class<D> elementType) {
233-
return this.responseMono.map(this::getField).map(field -> field.toEntityList(elementType));
238+
return this.responseMono.map(response -> {
239+
ResponseField field = getValidField(response);
240+
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
241+
});
234242
}
235243

236244
@Override
237245
public <D> Mono<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
238-
return this.responseMono.map(this::getField).map(field -> field.toEntityList(elementType));
246+
return this.responseMono.map(response -> {
247+
ResponseField field = getValidField(response);
248+
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
249+
});
239250
}
240251

241252
}
@@ -252,22 +263,28 @@ private static class DefaultRetrieveSubscriptionSpec extends RetrieveSpecSupport
252263

253264
@Override
254265
public <D> Flux<D> toEntity(Class<D> entityType) {
255-
return this.responseFlux.map(this::getField).map(field -> field.toEntity(entityType));
266+
return this.responseFlux.mapNotNull(this::getValidField).map(field -> field.toEntity(entityType));
256267
}
257268

258269
@Override
259270
public <D> Flux<D> toEntity(ParameterizedTypeReference<D> entityType) {
260-
return this.responseFlux.map(this::getField).map(field -> field.toEntity(entityType));
271+
return this.responseFlux.mapNotNull(this::getValidField).map(field -> field.toEntity(entityType));
261272
}
262273

263274
@Override
264275
public <D> Flux<List<D>> toEntityList(Class<D> elementType) {
265-
return this.responseFlux.map(this::getField).map(field -> field.toEntityList(elementType));
276+
return this.responseFlux.map(response -> {
277+
ResponseField field = getValidField(response);
278+
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
279+
});
266280
}
267281

268282
@Override
269283
public <D> Flux<List<D>> toEntityList(ParameterizedTypeReference<D> elementType) {
270-
return this.responseFlux.map(this::getField).map(field -> field.toEntityList(elementType));
284+
return this.responseFlux.map(response -> {
285+
ResponseField field = getValidField(response);
286+
return (field != null ? field.toEntityList(elementType) : Collections.emptyList());
287+
});
271288
}
272289

273290
}

spring-graphql/src/main/java/org/springframework/graphql/client/GraphQlClient.java

+17-13
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ interface RequestSpec {
155155

156156
/**
157157
* Execute a "subscription" request and return a stream of responses.
158-
* @return a {@code Flux} with a {@code ClientGraphQlResponse} for further
159-
* decoding of the response. The {@code Flux} may terminate as follows:
158+
* @return a {@code Flux} with responses that provide further options for
159+
* decoding of each response. The {@code Flux} may terminate as follows:
160160
* <ul>
161161
* <li>Completes if the subscription completes before the connection is closed.
162162
* <li>{@link SubscriptionErrorException} if the subscription ends with an error.
@@ -180,9 +180,10 @@ interface RetrieveSpec {
180180
/**
181181
* Decode the field to an entity of the given type.
182182
* @param entityType the type to convert to
183-
* @return {@code Mono} with the decoded entity, or a
184-
* {@link FieldAccessException} if the target field is not present or
185-
* has no value, checked via {@link ResponseField#hasValue()}.
183+
* @return {@code Mono} that provides the decoded entity, or completes
184+
* empty when the field is {@code null} but without errors, or ends with
185+
* a {@link FieldAccessException} if the target field is not present or
186+
* has no value.
186187
*/
187188
<D> Mono<D> toEntity(Class<D> entityType);
188189

@@ -194,9 +195,9 @@ interface RetrieveSpec {
194195
/**
195196
* Decode the field to a list of entities with the given type.
196197
* @param elementType the type of elements in the list
197-
* @return {@code Mono} with a list of decoded entities, possibly empty, or
198-
* a {@link FieldAccessException} if the target field is not present or
199-
* has no value, checked via {@link ResponseField#hasValue()}; the stream
198+
* @return {@code Mono} with a list of decoded entities, possibly an
199+
* empty list, or ends with {@link FieldAccessException} if the target
200+
* field is not present or has no value.
200201
*/
201202
<D> Mono<List<D>> toEntityList(Class<D> elementType);
202203

@@ -216,9 +217,11 @@ interface RetrieveSubscriptionSpec {
216217
/**
217218
* Decode the field to an entity of the given type.
218219
* @param entityType the type to convert to
219-
* @return {@code Mono} with the decoded entity, or a
220+
* @return decoded entities, one for each response, except responses
221+
* in which the field is {@code null} but without errors, or ending with
220222
* {@link FieldAccessException} if the target field is not present or
221-
* has no value, checked via {@link ResponseField#hasValue()}.
223+
* has no value in a given response; the stream may also end with a
224+
* {@link GraphQlTransportException}.
222225
*/
223226
<D> Flux<D> toEntity(Class<D> entityType);
224227

@@ -230,10 +233,11 @@ interface RetrieveSubscriptionSpec {
230233
/**
231234
* Decode the field to a list of entities with the given type.
232235
* @param elementType the type of elements in the list
233-
* @return lists of decoded entities, possibly empty, or a
236+
* @return lists of decoded entities, one for each response, except responses
237+
* in which the field is {@code null} but without errors, or ending with
234238
* {@link FieldAccessException} if the target field is not present or
235-
* has no value, checked via {@link ResponseField#hasValue()}; the stream
236-
* may also end with a range of {@link GraphQlTransportException} types.
239+
* has no value in a given response; the stream may also end with a
240+
* {@link GraphQlTransportException}.
237241
*/
238242
<D> Flux<List<D>> toEntityList(Class<D> elementType);
239243

spring-graphql/src/main/java/org/springframework/graphql/client/ResponseField.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
public interface ResponseField {
3535

3636
/**
37-
* Whether the field is valid and has a value.
37+
* Whether the field has a value.
3838
* <ul>
3939
* <li>{@code "true"} means the field is not {@code null} in which case there
4040
* is no field {@link #getError() error}. The field may still be partial and

spring-graphql/src/test/java/org/springframework/graphql/client/GraphQlClientTests.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,9 @@ void retrievePartialResponse() {
133133
String document = "fieldErrorResponse";
134134
initResponse(document, "{\"me\": {\"name\":null}}", errorForPath("/me/name"));
135135

136-
testRetrieveFieldAccessException(document, "me");
136+
MovieCharacter character = graphQlClient().document(document).retrieve("me").toEntity(MovieCharacter.class).block();
137+
assertThat(character).isNotNull().extracting(MovieCharacter::getName).isNull();
138+
137139
testRetrieveFieldAccessException(document, "me.name");
138140
}
139141

0 commit comments

Comments
 (0)