Skip to content

This removes the FetchedValue wrapping by default #3924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 10, 2025
Merged
78 changes: 47 additions & 31 deletions src/main/java/graphql/execution/ExecutionStrategy.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
* <p>
* The first phase (data fetching) is handled by the method {@link #fetchField(ExecutionContext, ExecutionStrategyParameters)}
* <p>
* The second phase (value completion) is handled by the methods {@link #completeField(ExecutionContext, ExecutionStrategyParameters, FetchedValue)}
* The second phase (value completion) is handled by the methods {@link #completeField(ExecutionContext, ExecutionStrategyParameters, Object)}
* and the other "completeXXX" methods.
* <p>
* The order of fields fetching and completion is up to the execution strategy. As the graphql specification
Expand Down Expand Up @@ -358,7 +358,7 @@ protected Object resolveFieldWithInfo(ExecutionContext executionContext, Executi

Object fetchedValueObj = fetchField(executionContext, parameters);
if (fetchedValueObj instanceof CompletableFuture) {
CompletableFuture<FetchedValue> fetchFieldFuture = (CompletableFuture<FetchedValue>) fetchedValueObj;
CompletableFuture<Object> fetchFieldFuture = (CompletableFuture<Object>) fetchedValueObj;
CompletableFuture<FieldValueInfo> result = fetchFieldFuture.thenApply((fetchedValue) ->
completeField(fieldDef, executionContext, parameters, fetchedValue));

Expand All @@ -367,10 +367,9 @@ protected Object resolveFieldWithInfo(ExecutionContext executionContext, Executi
return result;
} else {
try {
FetchedValue fetchedValue = (FetchedValue) fetchedValueObj;
FieldValueInfo fieldValueInfo = completeField(fieldDef, executionContext, parameters, fetchedValue);
FieldValueInfo fieldValueInfo = completeField(fieldDef, executionContext, parameters, fetchedValueObj);
fieldCtx.onDispatched();
fieldCtx.onCompleted(fetchedValue.getFetchedValue(), null);
fieldCtx.onCompleted(FetchedValue.getFetchedValue(fetchedValueObj), null);
return fieldValueInfo;
} catch (Exception e) {
return Async.exceptionallyCompletedFuture(e);
Expand All @@ -383,29 +382,29 @@ protected Object resolveFieldWithInfo(ExecutionContext executionContext, Executi
* {@link GraphQLFieldDefinition}.
* <p>
* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it
* in the query, hence the fieldList. However the first entry is representative of the field for most purposes.
* in the query, hence the fieldList. However, the first entry is representative of the field for most purposes.
*
* @param executionContext contains the top level execution parameters
* @param parameters contains the parameters holding the fields to be executed and source object
*
* @return a promise to a {@link FetchedValue} object or the {@link FetchedValue} itself
* @return a promise to a value object or the value itself. The value maybe a raw object OR a {@link FetchedValue}
*
* @throws NonNullableFieldWasNullException in the future if a non null field resolves to a null value
* @throws NonNullableFieldWasNullException in the future if a non-null field resolves to a null value
*/
@DuckTyped(shape = "CompletableFuture<FetchedValue> | FetchedValue")
@DuckTyped(shape = "CompletableFuture<FetchedValue|Object> | <FetchedValue|Object>")
protected Object fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
MergedField field = parameters.getField();
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();
GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field.getSingleField());
return fetchField(fieldDef, executionContext, parameters);
}

@DuckTyped(shape = "CompletableFuture<FetchedValue> | FetchedValue")
@DuckTyped(shape = "CompletableFuture<FetchedValue|Object> | <FetchedValue|Object>")
private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
executionContext.throwIfCancelled();

if (incrementAndCheckMaxNodesExceeded(executionContext)) {
return new FetchedValue(null, Collections.emptyList(), null);
return null;
}

MergedField field = parameters.getField();
Expand Down Expand Up @@ -486,9 +485,8 @@ private Object fetchField(GraphQLFieldDefinition fieldDef, ExecutionContext exec
}
});
CompletableFuture<Object> rawResultCF = engineRunningState.compose(handleCF, Function.identity());
CompletableFuture<FetchedValue> fetchedValueCF = rawResultCF
return rawResultCF
.thenApply(result -> unboxPossibleDataFetcherResult(executionContext, parameters, result));
return fetchedValueCF;
} else {
fetchCtx.onCompleted(fetchedObject, null);
return unboxPossibleDataFetcherResult(executionContext, parameters, fetchedObject);
Expand Down Expand Up @@ -520,9 +518,21 @@ protected Supplier<ExecutableNormalizedField> getNormalizedField(ExecutionContex
return () -> normalizedQuery.get().getNormalizedField(parameters.getField(), executionStepInfo.get().getObjectType(), executionStepInfo.get().getPath());
}

protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
Object result) {
/**
* If the data fetching returned a {@link DataFetcherResult} then it can contain errors and new local context
* and hence it gets turned into a {@link FetchedValue} but otherwise this method returns the unboxed
* value without the wrapper. This means its more efficient overall by default.
*
* @param executionContext the execution context in play
* @param parameters the parameters in play
* @param result the fetched raw object
*
* @return an unboxed value which can be a FetchedValue or an Object
*/
@DuckTyped(shape = "FetchedValue | Object")
protected Object unboxPossibleDataFetcherResult(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
Object result) {
if (result instanceof DataFetcherResult) {
DataFetcherResult<?> dataFetcherResult = (DataFetcherResult<?>) result;

Expand All @@ -538,8 +548,7 @@ protected FetchedValue unboxPossibleDataFetcherResult(ExecutionContext execution
Object unBoxedValue = executionContext.getValueUnboxer().unbox(dataFetcherResult.getData());
return new FetchedValue(unBoxedValue, dataFetcherResult.getErrors(), localContext);
} else {
Object unBoxedValue = executionContext.getValueUnboxer().unbox(result);
return new FetchedValue(unBoxedValue, ImmutableList.of(), parameters.getLocalContext());
return executionContext.getValueUnboxer().unbox(result);
}
}

Expand Down Expand Up @@ -577,29 +586,32 @@ protected <T> CompletableFuture<T> handleFetchingException(
private <T> CompletableFuture<T> asyncHandleException(DataFetcherExceptionHandler handler, DataFetcherExceptionHandlerParameters handlerParameters) {
//noinspection unchecked
return handler.handleException(handlerParameters).thenApply(
handlerResult -> (T) DataFetcherResult.<FetchedValue>newResult().errors(handlerResult.getErrors()).build()
handlerResult -> (T) DataFetcherResult.newResult().errors(handlerResult.getErrors()).build()
);
}

/**
* Called to complete a field based on the type of the field.
* <p>
* If the field is a scalar type, then it will be coerced and returned. However if the field type is an complex object type, then
* If the field is a scalar type, then it will be coerced and returned. However, if the field type is an complex object type, then
* the execution strategy will be called recursively again to execute the fields of that type before returning.
* <p>
* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it
* in the query, hence the fieldList. However the first entry is representative of the field for most purposes.
* in the query, hence the fieldList. However, the first entry is representative of the field for most purposes.
*
* @param executionContext contains the top level execution parameters
* @param parameters contains the parameters holding the fields to be executed and source object
* @param fetchedValue the fetched raw value
* @param fetchedValue the fetched raw value or perhaps a {@link FetchedValue} wrapper of that value
*
* @return a {@link FieldValueInfo}
*
* @throws NonNullableFieldWasNullException in the {@link FieldValueInfo#getFieldValueFuture()} future
* if a nonnull field resolves to a null value
*/
protected FieldValueInfo completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) {
protected FieldValueInfo completeField(ExecutionContext executionContext,
ExecutionStrategyParameters parameters,
@DuckTyped(shape = "Object | FetchedValue")
Object fetchedValue) {
executionContext.throwIfCancelled();

Field field = parameters.getField().getSingleField();
Expand All @@ -608,7 +620,7 @@ protected FieldValueInfo completeField(ExecutionContext executionContext, Execut
return completeField(fieldDef, executionContext, parameters, fetchedValue);
}

private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters, FetchedValue fetchedValue) {
private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionContext executionContext, ExecutionStrategyParameters parameters, Object fetchedValue) {
GraphQLObjectType parentType = (GraphQLObjectType) parameters.getExecutionStepInfo().getUnwrappedNonNullType();
ExecutionStepInfo executionStepInfo = createExecutionStepInfo(executionContext, parameters, fieldDef, parentType);

Expand All @@ -618,9 +630,11 @@ private FieldValueInfo completeField(GraphQLFieldDefinition fieldDef, ExecutionC
instrumentationParams, executionContext.getInstrumentationState()
));

ExecutionStrategyParameters newParameters = parameters.transform(executionStepInfo,
fetchedValue.getLocalContext(),
fetchedValue.getFetchedValue());
ExecutionStrategyParameters newParameters = parameters.transform(
executionStepInfo,
FetchedValue.getLocalContext(fetchedValue, parameters.getLocalContext()),
FetchedValue.getFetchedValue(fetchedValue)
);

FieldValueInfo fieldValueInfo = completeValue(executionContext, newParameters);
ctxCompleteField.onDispatched();
Expand Down Expand Up @@ -777,12 +791,14 @@ protected FieldValueInfo completeValueForList(ExecutionContext executionContext,

ExecutionStepInfo stepInfoForListElement = executionStepInfoFactory.newExecutionStepInfoForListElement(executionStepInfo, indexedPath);

FetchedValue value = unboxPossibleDataFetcherResult(executionContext, parameters, item);
Object fetchedValue = unboxPossibleDataFetcherResult(executionContext, parameters, item);

ExecutionStrategyParameters newParameters = parameters.transform(stepInfoForListElement,
ExecutionStrategyParameters newParameters = parameters.transform(
stepInfoForListElement,
indexedPath,
value.getLocalContext(),
value.getFetchedValue());
FetchedValue.getLocalContext(fetchedValue, parameters.getLocalContext()),
FetchedValue.getFetchedValue(fetchedValue)
);

fieldValueInfos.add(completeValue(executionContext, newParameters));
index++;
Expand Down
35 changes: 34 additions & 1 deletion src/main/java/graphql/execution/FetchedValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import java.util.List;

/**
* Note: This is returned by {@link InstrumentationFieldCompleteParameters#getFetchedValue()}
* Note: This MAY be returned by {@link InstrumentationFieldCompleteParameters#getFetchedObject()}
* and therefore part of the public despite never used in a method signature.
*/
@PublicApi
Expand All @@ -17,6 +17,39 @@ public class FetchedValue {
private final Object localContext;
private final ImmutableList<GraphQLError> errors;

/**
* This allows you to get to the underlying fetched value depending on whether the source
* value is a {@link FetchedValue} or not
*
* @param sourceValue the source value in play
*
* @return the {@link FetchedValue#getFetchedValue()} if its wrapped otherwise the source value itself
*/
public static Object getFetchedValue(Object sourceValue) {
if (sourceValue instanceof FetchedValue) {
return ((FetchedValue) sourceValue).fetchedValue;
} else {
return sourceValue;
}
}

/**
* This allows you to get to the local context depending on whether the source
* value is a {@link FetchedValue} or not
*
* @param sourceValue the source value in play
* @param defaultLocalContext the default local context to use
*
* @return the {@link FetchedValue#getFetchedValue()} if its wrapped otherwise the default local context
*/
public static Object getLocalContext(Object sourceValue, Object defaultLocalContext) {
if (sourceValue instanceof FetchedValue) {
return ((FetchedValue) sourceValue).localContext;
} else {
return defaultLocalContext;
}
}

public FetchedValue(Object fetchedValue, List<GraphQLError> errors, Object localContext) {
this.fetchedValue = fetchedValue;
this.errors = ImmutableList.copyOf(errors);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ private boolean keepOrdered(GraphQLContext graphQLContext) {
private CompletableFuture<Publisher<Object>> createSourceEventStream(ExecutionContext executionContext, ExecutionStrategyParameters parameters) {
ExecutionStrategyParameters newParameters = firstFieldOfSubscriptionSelection(executionContext, parameters, false);

CompletableFuture<FetchedValue> fieldFetched = Async.toCompletableFuture(fetchField(executionContext, newParameters));
CompletableFuture<Object> fieldFetched = Async.toCompletableFuture(fetchField(executionContext, newParameters));
return fieldFetched.thenApply(fetchedValue -> {
Object publisher = fetchedValue.getFetchedValue();
Object publisher = FetchedValue.getFetchedValue(fetchedValue);
return mkReactivePublisher(publisher);
});
}
Expand Down Expand Up @@ -168,7 +168,7 @@ private CompletableFuture<ExecutionResult> executeSubscriptionEvent(ExecutionCon
i13nFieldParameters, executionContext.getInstrumentationState()
));

FetchedValue fetchedValue = unboxPossibleDataFetcherResult(newExecutionContext, newParameters, eventPayload);
Object fetchedValue = unboxPossibleDataFetcherResult(newExecutionContext, newParameters, eventPayload);
FieldValueInfo fieldValueInfo = completeField(newExecutionContext, newParameters, fetchedValue);
executionContext.getDataLoaderDispatcherStrategy().newSubscriptionExecution(fieldValueInfo, newParameters.getDeferredCallContext());
CompletableFuture<ExecutionResult> overallResult = fieldValueInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ public ExecutionStepInfo getExecutionStepInfo() {
return executionStepInfo.get();
}

public Object getFetchedValue() {
/**
* This returns the object that was fetched, ready to be completed as a value. This can sometimes be a {@link graphql.execution.FetchedValue} object
* but most often it's a simple POJO.
*
* @return the object was fetched, ready to be completed as a value.
*/
public Object getFetchedObject() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the hard breaking change - the new method means they HAVE to handle it should they be using the old method

return fetchedValue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,19 @@ public BreadthFirstExecutionTestStrategy() {
public CompletableFuture<ExecutionResult> execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException {
MergedSelectionSet fields = parameters.getFields();

Map<String, FetchedValue> fetchedValues = new LinkedHashMap<>();
Map<String, Object> fetchedValues = new LinkedHashMap<>();

// first fetch every value
for (String fieldName : fields.keySet()) {
FetchedValue fetchedValue = fetchField(executionContext, parameters, fields, fieldName);
Object fetchedValue = fetchField(executionContext, parameters, fields, fieldName);
fetchedValues.put(fieldName, fetchedValue);
}

// then for every fetched value, complete it
Map<String, Object> results = new LinkedHashMap<>();
for (String fieldName : fetchedValues.keySet()) {
MergedField currentField = fields.getSubField(fieldName);
FetchedValue fetchedValue = fetchedValues.get(fieldName);
Object fetchedValue = fetchedValues.get(fieldName);

ResultPath fieldPath = parameters.getPath().segment(fieldName);
ExecutionStrategyParameters newParameters = parameters
Expand All @@ -51,17 +51,17 @@ public CompletableFuture<ExecutionResult> execute(ExecutionContext executionCont
return CompletableFuture.completedFuture(new ExecutionResultImpl(results, executionContext.getErrors()));
}

private FetchedValue fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedSelectionSet fields, String fieldName) {
private Object fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, MergedSelectionSet fields, String fieldName) {
MergedField currentField = fields.getSubField(fieldName);

ResultPath fieldPath = parameters.getPath().segment(fieldName);
ExecutionStrategyParameters newParameters = parameters
.transform(builder -> builder.field(currentField).path(fieldPath));

return Async.<FetchedValue>toCompletableFuture(fetchField(executionContext, newParameters)).join();
return Async.toCompletableFuture(fetchField(executionContext, newParameters)).join();
}

private void completeValue(ExecutionContext executionContext, Map<String, Object> results, String fieldName, FetchedValue fetchedValue, ExecutionStrategyParameters newParameters) {
private void completeValue(ExecutionContext executionContext, Map<String, Object> results, String fieldName, Object fetchedValue, ExecutionStrategyParameters newParameters) {
Object resolvedResult = completeField(executionContext, newParameters, fetchedValue).getFieldValueFuture().join();
results.put(fieldName, resolvedResult);
}
Expand Down
Loading