Upload Files With GraphQL in Java
File uploads are a common feature in modern applications, and GraphQL can handle them with some customization. GraphQL doesn’t natively support file uploads out of the box, and managing file uploads in a GraphQL API involves a few additional steps compared to traditional REST APIs. This article will guide us through creating a Spring Boot application that enables file uploads via GraphQL.
1. Introduction to File Upload with GraphQL
Standard GraphQL doesn’t support file uploads natively. GraphQL typically works with JSON-based queries, and files like images or documents don’t fit into this format. However, we can work around this limitation by using the multipart form data format, which allows files to be transmitted alongside the GraphQL query in a single request.
By defining a custom scalar type called Upload, we can handle file uploads within the GraphQL framework, making it easy to integrate file handling into our GraphQL API.
2. Dependencies
In this section, we’ll add the required dependencies to set up our GraphQL application and enable file uploads. Set up a Spring Boot project which can be done by using Spring Initializr to create a project with the following necessary dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
2. Define Schema for File Upload
In GraphQL, scalars are used to define custom data types. Here, we define a Upload scalar to represent file uploads. Create a new file called src/main/resources/graphql/mutation.graphqls and add the following content to define the upload operation.
This GraphQL schema defines the structure for a simple file upload mutation.
scalar Upload
type Mutation {
uploadFile(file: Upload!): String
}
type Query {
getFile: String
}
In this code, the uploadFile mutation expects one parameter: a file of type Upload. The custom scalar type Upload will be mapped to a MultipartFile, allowing us to process the file upload.
3. Handle Upload Scalar with Coercing
In the context of GraphQL, coercing refers to the process of converting data between the format used by the GraphQL schema and the format received or sent in the API requests and responses. This is important for custom scalar types or complex data formats.
To handle the Upload scalar, we need to implement a custom coercing class that manages how the MultipartFile object is processed.
import graphql.schema.Coercing;
import graphql.schema.CoercingParseLiteralException;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;
import org.springframework.web.multipart.MultipartFile;
public class UploadCoercing implements Coercing<MultipartFile, MultipartFile> {
@Override
public MultipartFile serialize(Object dataFetcherResult) throws CoercingSerializeException {
throw new CoercingSerializeException("Upload is an input-only type");
}
@Override
public MultipartFile parseValue(Object input) throws CoercingParseValueException {
if (input instanceof MultipartFile) {
return (MultipartFile) input;
}
throw new CoercingParseValueException(
String.format("Expected a 'MultipartFile' like object but was '%s'.", input != null ? input.getClass() : null)
);
}
@Override
public MultipartFile parseLiteral(Object input) throws CoercingParseLiteralException {
throw new CoercingParseLiteralException("Parsing literal of 'MultipartFile' is not supported");
}
}
The UploadCoercing class defines how the Upload scalar is interpreted. The parseValue method ensures that the input is of type MultipartFile, and it throws exceptions for unsupported operations like serialization and parsing literals. This coercing logic is essential to safely process file uploads and prevent unsupported operations.
4. Handling Multipart Requests
This section contains the GraphqlMultipartHandler class, which processes multipart form-data requests, extracts file uploads, and maps them to GraphQL query variables.
public class GraphqlMultipartHandler {
// A handler to process GraphQL requests
private final WebGraphQlHandler graphQlHandler;
// ObjectMapper used to convert between JSON and Java objects
private final ObjectMapper objectMapper;
// Constructor that initializes the handler and object mapper with null checks
public GraphqlMultipartHandler(WebGraphQlHandler graphQlHandler, ObjectMapper objectMapper) {
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
Assert.notNull(objectMapper, "ObjectMapper is required");
this.graphQlHandler = graphQlHandler;
this.objectMapper = objectMapper;
}
// Supported media types for the GraphQL response (GraphQL and JSON)
public static final List<MediaType> SUPPORTED_RESPONSE_MEDIA_TYPES
= Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);
private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
// Main method to handle the incoming multipart GraphQL request
public ServerResponse handleRequest(ServerRequest serverRequest) throws ServletException {
// Read the 'operations' parameter from the request
Optional<String> operation = serverRequest.param("operations");
// Read the 'map' parameter which maps files to query variables
Optional<String> mapParam = serverRequest.param("map");
// Parse the 'operations' JSON into a Map
Map<String, Object> inputQuery = readJson(operation, new TypeReference<>() {});
// Extract query variables from the input query
final Map<String, Object> queryVariables;
if (inputQuery.containsKey("variables")) {
queryVariables = (Map<String, Object>) inputQuery.get("variables");
} else {
queryVariables = new HashMap<>();
}
// Handle extensions if present in the request
Map<String, Object> extensions = new HashMap<>();
if (inputQuery.containsKey("extensions")) {
extensions = (Map<String, Object>) inputQuery.get("extensions");
}
// Read the file parts from the multipart body
Map<String, MultipartFile> fileParams = readMultipartBody(serverRequest);
// Parse the 'map' JSON to associate files with variables
Map<String, List<String>> fileMapInput = readJson(mapParam, new TypeReference<>() {});
// Map each file to its corresponding variable path in the GraphQL query
fileMapInput.forEach((String fileKey, List<String> objectPaths) -> {
MultipartFile file = fileParams.get(fileKey);
if (file != null) {
objectPaths.forEach((String objectPath) -> {
MultipartVariableMapper.mapVariable(
objectPath,
queryVariables,
file
);
});
}
});
// Extract the actual GraphQL query and operation name
String query = (String) inputQuery.get("query");
String opName = (String) inputQuery.get("operationName");
// Remote address and cookies (if needed) for the request
InetSocketAddress remoteAddress = null;
MultiValueMap<String, HttpCookie> cookies = null;
// Build the request body including query, variables, and extensions
Map<String, Object> body = new HashMap<>();
body.put("query", query);
body.put("operationName", StringUtils.hasText(opName) ? opName : "");
body.put("variables", queryVariables);
body.put("extensions", extensions);
// Create a WebGraphQlRequest that wraps all the information required for processing
WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(serverRequest.uri(),
serverRequest.headers().asHttpHeaders(), cookies,
remoteAddress, queryVariables,
body, this.idGenerator.generateId().toString(), LocaleContextHolder.getLocale()
);
if (logger.isDebugEnabled()) {
logger.debug("Executing: " + graphQlRequest);
}
// Execute the request and return the server response as an asynchronous result
Mono<ServerResponse> responseMono = this.graphQlHandler.handleRequest(graphQlRequest)
.map(response -> {
if (logger.isDebugEnabled()) {
logger.debug("Execution complete");
}
// Build the response with appropriate headers and content type
ServerResponse.BodyBuilder builder = ServerResponse.ok();
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
builder.contentType(selectResponseMediaType(serverRequest));
return builder.body(response.toMap());
});
// Return the async server response
return ServerResponse.async(responseMono);
}
}
The GraphqlMultipartHandler class is responsible for handling multipart file uploads in GraphQL requests. It integrates a WebGraphQlHandler for processing the core GraphQL logic and an ObjectMapper for JSON parsing. The main functionality involves reading the incoming request, extracting the GraphQL query and associated variables, mapping file uploads to the appropriate GraphQL query parameters, and finally, executing the request and returning the appropriate response.
The handler supports both GraphQL and JSON media types and is designed to handle multipart forms, mapping files to GraphQL variables via a map parameter. The handleRequest method reads the file and query from the multipart body, logs the request, and processes it asynchronously. It parses the operation and maps parameters from the request, ensuring that uploaded files are correctly assigned to the variables in the query. The response is then built using appropriate headers and media types.
The class also includes utility methods like readJson for parsing JSON data and readMultipartBody for handling the file content. The full source code of this class can be downloaded from the link provided at the end of this article.
5. File Upload Data Fetcher
In this part of the article, we create a DataFetcher to manage the file upload mutation. The GraphQLFileUploader works with the FileStorageService to save the file and return the file path after a successful upload.
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
@Component
public class GraphQLFileUploader implements DataFetcher<String> {
private final FileStorageService fileStorageService;
public GraphQLFileUploader(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
@Override
public String get(DataFetchingEnvironment environment) {
MultipartFile file = environment.getArgument("file");
String storedFilePath = fileStorageService.store(file);
return String.format("File stored at: %s", storedFilePath);
}
}
The GraphQLFileUploader class is a Spring component that handles file uploads in a GraphQL API by implementing the DataFetcher interface. It retrieves the file from the DataFetchingEnvironment during a GraphQL mutation and uses the FileStorageService to store the file. In the get() method, the file is extracted from the mutation request, passed to the FileStorageService for saving, and a confirmation message with the file’s storage path is returned.
5.1 File Storage Service
In this section, we define the FileStorageService class, which handles the storage of uploaded files.
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
@Service
public class FileStorageService {
private final Path rootLocation = Paths.get("uploads");
public FileStorageService() {
try {
Files.createDirectories(rootLocation);
} catch (IOException e) {
throw new RuntimeException("Failed to initialize the storage location.", e);
}
}
public String store(MultipartFile file) {
try {
if (file.isEmpty()) {
throw new RuntimeException("Failed to store empty file.");
}
Path destination = rootLocation.resolve(
Paths.get(Objects.requireNonNull(file.getOriginalFilename())))
.normalize().toAbsolutePath();
file.transferTo(destination);
return String.format("File uploaded successfully: %s", destination);
} catch (IOException e) {
throw new RuntimeException("Failed to store file.", e);
}
}
}
The FileStorageService class handles file storage in the application. It initializes with an uploads directory and ensures it exists. If directory creation fails, an exception is thrown. The store method saves the MultipartFile to the specified directory (a directory named uploads, which is located relative to the application’s working directory).
6. GraphQL Configuration
This section covers configuring the GraphQL server to recognize the Upload scalar and route file uploads properly.
import com.fasterxml.jackson.databind.ObjectMapper;
import static com.jcg.GraphqlMultipartHandler.SUPPORTED_RESPONSE_MEDIA_TYPES;
import graphql.schema.GraphQLScalarType;
import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;
import org.springframework.boot.autoconfigure.graphql.GraphQlProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.server.WebGraphQlHandler;
import org.springframework.http.MediaType;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
import org.springframework.web.servlet.function.RequestPredicates;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
@Configuration
public class GraphqlConfiguration {
private final GraphQLFileUploader fileUploadDataFetcher;
public GraphqlConfiguration(GraphQLFileUploader fileUploadDataFetcher) {
this.fileUploadDataFetcher = fileUploadDataFetcher;
}
@Bean
public RuntimeWiringConfigurer runtimeWiringConfigurer() {
return (builder) -> builder
.type(newTypeWiring("Mutation").dataFetcher("uploadFile", fileUploadDataFetcher))
.scalar(GraphQLScalarType.newScalar()
.name("Upload")
.coercing(new UploadCoercing())
.build());
}
@Bean
@Order(1)
public RouterFunction<ServerResponse> graphQlMultipartRouterFunction(
GraphQlProperties properties,
WebGraphQlHandler webGraphQlHandler,
ObjectMapper objectMapper
) {
String path = properties.getPath();
RouterFunctions.Builder builder = RouterFunctions.route();
GraphqlMultipartHandler graphqlMultipartHandler = new GraphqlMultipartHandler(webGraphQlHandler, objectMapper);
builder = builder.POST(path, RequestPredicates.contentType(MULTIPART_FORM_DATA)
.and(RequestPredicates.accept(SUPPORTED_RESPONSE_MEDIA_TYPES.toArray(MediaType[]::new))), graphqlMultipartHandler::handleRequest);
return builder.build();
}
}
The GraphqlConfiguration class configures GraphQL for handling file uploads. The runtimeWiringConfigurer method sets up a data fetcher for the uploadFile mutation and defines a custom Upload scalar type with the UploadCoercing implementation. This method ensures that file upload operations are correctly wired in the GraphQL schema.
The graphQlMultipartRouterFunction method establishes routing for multipart form-data requests. It creates a RouterFunction that directs POST requests with MULTIPART_FORM_DATA content to the GraphqlMultipartHandler, which processes the file uploads. This setup enables efficient handling of file uploads in the GraphQL API.
7. Testing the File Upload API
We can test the file upload API using curl commands. Here’s an example curl request (replace the file path with your actual file location):
curl --location --request POST 'http://localhost:8080/graphql' --form 'operations={"query": "mutation UploadFile($file: Upload!) { uploadFile(file: $file) }", "variables": {"file": null}}' --form 'map={"file": ["variables.file"]}' --form 'file=@"/Users/omozegieaziegbe/Downloads/phoneicon.jpeg"'
The POST request to the GraphQL endpoint is structured to handle file uploads using multipart form-data. The operations form field specifies the GraphQL mutation UploadFile, which requires a file parameter. This field includes the GraphQL query and the variables for the mutation, detailing what operation to perform and which data to use.
The map form field links the uploaded file to the mutation variable variables.file, ensuring the server correctly associates the file with the mutation parameter. The file form field includes the actual file data, provided via a file path. Together, these components enable the file to be sent and processed correctly by the GraphQL server.
Upon a successful file upload POST request, the response will confirm the file was uploaded successfully. Here’s what to expect in the output:
8. Conclusion
In this article, we explored how to implement file upload functionality in a Java GraphQL application using Spring Boot. We covered handling multipart requests and creating a DataFetcher to manage file uploads. By the end, we demonstrated how to test the implementation using a sample curl request. With these steps, we can incorporate file uploads into our own GraphQL APIs.
9. Download the Source Code
This article focuses on implementing Java GraphQL upload file functionality.
You can download the full source code of this example here: java graphql upload file





