GraphQL with RSQL/FIQL: Dynamic Query Filtering for Modern APIs

Learn how to integrate RSQL (RESTful Service Query Language) with GraphQL to enable powerful, dynamic filtering capabilities. Build flexible APIs that combine GraphQL's type safety with RSQL's expressive query syntax.

GT
Gonnect Team
January 14, 202411 min readView on GitHub
JavaGraphQLRSQLSpring BootJPA

Introduction

Building APIs that support flexible querying is a common challenge in modern software development. While GraphQL provides excellent type safety and allows clients to request exactly the data they need, it traditionally requires predefined query parameters. RSQL (RESTful Service Query Language), also known as FIQL (Feed Item Query Language), offers a powerful alternative - a query language that enables dynamic, SQL-like filtering through URL parameters.

What if you could combine the best of both worlds? The graphql-rsql project demonstrates exactly this - integrating RSQL's expressive filtering capabilities directly into GraphQL queries, giving clients unprecedented flexibility in data retrieval.

Key Insight: By combining GraphQL with RSQL, you enable clients to construct complex queries dynamically without requiring backend changes for each new filter combination.

Understanding RSQL/FIQL

What is RSQL?

RSQL is a query language for parametrized filtering of entries in RESTful APIs. It provides a simple, URL-friendly syntax for expressing complex search queries that would otherwise require multiple endpoints or custom query parameters.

RSQL Syntax Overview

OperatorSymbolExampleDescription
Equal==name==JohnExact match
Not Equal!=status!=deletedExclusion
Greater Than=gt= or >age=gt=18Numeric comparison
Greater Equal=ge= or >=score=ge=100Inclusive comparison
Less Than=lt= or <price=lt=50Numeric comparison
Less Equal=le= or <=count=le=10Inclusive comparison
In=in=status=in=(active,pending)Set membership
Out=out=role=out=(guest,banned)Set exclusion
Like=like=name=like=*john*Pattern matching
AND;age=gt=18;status==activeLogical conjunction
OR,role==admin,role==modLogical disjunction

RSQL Query Examples

# Simple equality
name==Buddy

# Numeric comparison
age=gt=3

# Combined conditions (AND)
species==dog;age=gt=2

# Multiple options (OR)
status==available,status==pending

# Complex query
species==cat;(age=lt=2,vaccinated==true)

Architecture Overview

The graphql-rsql project implements a clean architecture that bridges GraphQL and RSQL:

+------------------------------------------------------------------+
|                    GRAPHQL-RSQL ARCHITECTURE                      |
+------------------------------------------------------------------+
|                                                                   |
|    +---------------------------+                                  |
|    |     GraphQL Query         |                                  |
|    |  query { pets(filter:     |                                  |
|    |    "species==dog;age>2")  |                                  |
|    |    { id name age } }      |                                  |
|    +------------+--------------+                                  |
|                 |                                                 |
|        +--------v--------+                                        |
|        |  GraphQL Resolver |                                      |
|        |  (PetResolver)   |                                       |
|        +--------+---------+                                       |
|                 |                                                 |
|        +--------v---------+                                       |
|        |   RSQL Parser    |                                       |
|        | (cz.jirutka.rsql)|                                       |
|        +--------+---------+                                       |
|                 |                                                 |
|        +--------v---------+                                       |
|        |  RSQL Visitor    |                                       |
|        | (AST Traversal)  |                                       |
|        +--------+---------+                                       |
|                 |                                                 |
|        +--------v-----------+                                     |
|        | JPA Specification  |                                     |
|        | (RsqlSpecification)|                                     |
|        +--------+-----------+                                     |
|                 |                                                 |
|        +--------v--------+                                        |
|        |  Spring Data    |                                        |
|        |  Repository     |                                        |
|        +--------+--------+                                        |
|                 |                                                 |
|        +--------v--------+                                        |
|        |    Database     |                                        |
|        |   (Derby/SQL)   |                                        |
|        +-----------------+                                        |
|                                                                   |
+------------------------------------------------------------------+

Project Setup

Maven Dependencies

The project uses Spring Boot with GraphQL and RSQL parser libraries:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.gonnect.graphql</groupId>
    <artifactId>graphql-rsql</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- GraphQL -->
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-java-tools</artifactId>
            <version>5.2.4</version>
        </dependency>
        <dependency>
            <groupId>com.graphql-java</groupId>
            <artifactId>graphql-spring-boot-starter</artifactId>
            <version>5.0.2</version>
        </dependency>

        <!-- RSQL Parser -->
        <dependency>
            <groupId>cz.jirutka.rsql</groupId>
            <artifactId>rsql-parser</artifactId>
            <version>2.1.0</version>
        </dependency>

        <!-- Spring Data JPA -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.hibernate</groupId>
                    <artifactId>hibernate-entitymanager</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.hibernate</groupId>
                    <artifactId>hibernate-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- EclipseLink JPA Provider -->
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa</artifactId>
            <version>2.7.4</version>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.20</version>
        </dependency>
    </dependencies>
</project>

Implementation Deep Dive

Domain Entity

First, define the domain entity that will be queried:

package com.gonnect.graphql.entities;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;

import javax.persistence.*;

@Entity
@Table(name = "pets")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Pet {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String species;

    private Integer age;

    private String breed;

    @Enumerated(EnumType.STRING)
    private PetStatus status;

    private Boolean vaccinated;

    private String ownerName;
}

RSQL Search Operations

Define the supported RSQL operators and their mappings:

package com.gonnect.graphql.rsql;

import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import cz.jirutka.rsql.parser.ast.RSQLOperators;

public enum RsqlSearchOperation {

    EQUAL(RSQLOperators.EQUAL),
    NOT_EQUAL(RSQLOperators.NOT_EQUAL),
    GREATER_THAN(RSQLOperators.GREATER_THAN),
    GREATER_THAN_OR_EQUAL(RSQLOperators.GREATER_THAN_OR_EQUAL),
    LESS_THAN(RSQLOperators.LESS_THAN),
    LESS_THAN_OR_EQUAL(RSQLOperators.LESS_THAN_OR_EQUAL),
    IN(RSQLOperators.IN),
    NOT_IN(RSQLOperators.NOT_IN);

    private final ComparisonOperator operator;

    RsqlSearchOperation(ComparisonOperator operator) {
        this.operator = operator;
    }

    public static RsqlSearchOperation getSimpleOperator(ComparisonOperator operator) {
        for (RsqlSearchOperation operation : values()) {
            if (operation.operator.equals(operator)) {
                return operation;
            }
        }
        return null;
    }
}

JPA Specification Builder

The core of the RSQL integration - converting RSQL expressions to JPA Specifications:

package com.gonnect.graphql.rsql;

import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.*;
import java.util.List;
import java.util.stream.Collectors;

public class RsqlSpecification<T> implements Specification<T> {

    private final String property;
    private final RsqlSearchOperation operation;
    private final List<String> arguments;

    public RsqlSpecification(String property, RsqlSearchOperation operation,
                            List<String> arguments) {
        this.property = property;
        this.operation = operation;
        this.arguments = arguments;
    }

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
                                 CriteriaBuilder builder) {

        List<Object> args = castArguments(root);
        Object argument = args.get(0);

        switch (operation) {
            case EQUAL:
                if (argument instanceof String) {
                    return builder.like(root.get(property),
                        argument.toString().replace('*', '%'));
                } else if (argument == null) {
                    return builder.isNull(root.get(property));
                }
                return builder.equal(root.get(property), argument);

            case NOT_EQUAL:
                if (argument instanceof String) {
                    return builder.notLike(root.get(property),
                        argument.toString().replace('*', '%'));
                } else if (argument == null) {
                    return builder.isNotNull(root.get(property));
                }
                return builder.notEqual(root.get(property), argument);

            case GREATER_THAN:
                return builder.greaterThan(root.get(property),
                    argument.toString());

            case GREATER_THAN_OR_EQUAL:
                return builder.greaterThanOrEqualTo(root.get(property),
                    argument.toString());

            case LESS_THAN:
                return builder.lessThan(root.get(property),
                    argument.toString());

            case LESS_THAN_OR_EQUAL:
                return builder.lessThanOrEqualTo(root.get(property),
                    argument.toString());

            case IN:
                return root.get(property).in(args);

            case NOT_IN:
                return builder.not(root.get(property).in(args));

            default:
                return null;
        }
    }

    private List<Object> castArguments(Root<T> root) {
        Class<?> type = root.get(property).getJavaType();

        return arguments.stream().map(arg -> {
            if (type.equals(Integer.class) || type.equals(int.class)) {
                return Integer.parseInt(arg);
            } else if (type.equals(Long.class) || type.equals(long.class)) {
                return Long.parseLong(arg);
            } else if (type.equals(Boolean.class) || type.equals(boolean.class)) {
                return Boolean.parseBoolean(arg);
            } else if (type.equals(Double.class) || type.equals(double.class)) {
                return Double.parseDouble(arg);
            } else if (type.isEnum()) {
                return Enum.valueOf((Class<Enum>) type, arg);
            }
            return arg;
        }).collect(Collectors.toList());
    }
}

RSQL Visitor Implementation

The visitor pattern traverses the RSQL AST and builds JPA Specifications:

package com.gonnect.graphql.rsql;

import cz.jirutka.rsql.parser.ast.*;
import org.springframework.data.jpa.domain.Specification;

public class PetRsqlVisitor<T> implements RSQLVisitor<Specification<T>, Void> {

    @Override
    public Specification<T> visit(AndNode node, Void param) {
        return node.getChildren().stream()
            .map(n -> n.accept(this))
            .reduce(Specification::and)
            .orElse(null);
    }

    @Override
    public Specification<T> visit(OrNode node, Void param) {
        return node.getChildren().stream()
            .map(n -> n.accept(this))
            .reduce(Specification::or)
            .orElse(null);
    }

    @Override
    public Specification<T> visit(ComparisonNode node, Void param) {
        return new RsqlSpecification<>(
            node.getSelector(),
            RsqlSearchOperation.getSimpleOperator(node.getOperator()),
            node.getArguments()
        );
    }
}

Pet Specification Builder

A builder class that orchestrates the RSQL parsing and specification creation:

package com.gonnect.graphql.rsql;

import cz.jirutka.rsql.parser.RSQLParser;
import cz.jirutka.rsql.parser.ast.Node;
import org.springframework.data.jpa.domain.Specification;

public class PetBuilder {

    public static Specification<Pet> buildSpecification(String rsqlQuery) {
        if (rsqlQuery == null || rsqlQuery.isEmpty()) {
            return Specification.where(null);
        }

        RSQLParser parser = new RSQLParser();
        Node rootNode = parser.parse(rsqlQuery);

        PetRsqlVisitor<Pet> visitor = new PetRsqlVisitor<>();
        return rootNode.accept(visitor);
    }
}

GraphQL Schema

Define the GraphQL schema with RSQL filter support:

type Query {
    pets(filter: String): [Pet!]!
    pet(id: ID!): Pet
}

type Mutation {
    createPet(input: PetInput!): Pet!
    updatePet(id: ID!, input: PetInput!): Pet
    deletePet(id: ID!): Boolean!
}

type Pet {
    id: ID!
    name: String!
    species: String!
    age: Int
    breed: String
    status: PetStatus!
    vaccinated: Boolean
    ownerName: String
}

enum PetStatus {
    AVAILABLE
    PENDING
    ADOPTED
}

input PetInput {
    name: String!
    species: String!
    age: Int
    breed: String
    status: PetStatus
    vaccinated: Boolean
    ownerName: String
}

GraphQL Resolver

The resolver integrates RSQL filtering with GraphQL queries:

package com.gonnect.graphql.resolvers;

import com.coxautodev.graphql.tools.GraphQLQueryResolver;
import com.gonnect.graphql.entities.Pet;
import com.gonnect.graphql.repository.PetRepository;
import com.gonnect.graphql.rsql.PetBuilder;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class PetQueryResolver implements GraphQLQueryResolver {

    private final PetRepository petRepository;

    public PetQueryResolver(PetRepository petRepository) {
        this.petRepository = petRepository;
    }

    public List<Pet> pets(String filter) {
        if (filter == null || filter.isEmpty()) {
            return petRepository.findAll();
        }

        Specification<Pet> specification = PetBuilder.buildSpecification(filter);
        return petRepository.findAll(specification);
    }

    public Pet pet(Long id) {
        return petRepository.findById(id).orElse(null);
    }
}

Spring Data Repository

The repository extends JpaSpecificationExecutor for specification support:

package com.gonnect.graphql.repository;

import com.gonnect.graphql.entities.Pet;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

@Repository
public interface PetRepository extends JpaRepository<Pet, Long>,
                                       JpaSpecificationExecutor<Pet> {
}

GraphQL Query Examples

Basic Filtering

# Find all dogs
query {
  pets(filter: "species==dog") {
    id
    name
    breed
    age
  }
}

# Find pets older than 3 years
query {
  pets(filter: "age=gt=3") {
    id
    name
    species
    age
  }
}

Complex Queries

# Find vaccinated dogs over 2 years old
query {
  pets(filter: "species==dog;age=gt=2;vaccinated==true") {
    id
    name
    breed
    age
    vaccinated
  }
}

# Find available or pending pets
query {
  pets(filter: "status==AVAILABLE,status==PENDING") {
    id
    name
    status
    ownerName
  }
}

# Find cats or dogs that are available
query {
  pets(filter: "(species==cat,species==dog);status==AVAILABLE") {
    id
    name
    species
    status
  }
}

Pattern Matching

# Find pets with names starting with 'B'
query {
  pets(filter: "name==B*") {
    id
    name
    species
  }
}

# Find pets with 'buddy' anywhere in the name
query {
  pets(filter: "name==*buddy*") {
    id
    name
  }
}

Using IN Operator

# Find pets of specific breeds
query {
  pets(filter: "breed=in=(Labrador,Golden Retriever,Beagle)") {
    id
    name
    breed
    species
  }
}

Advanced Configuration

Custom Operators

Extend RSQL with custom operators for domain-specific queries:

package com.gonnect.graphql.rsql;

import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import java.util.Set;

public class CustomOperators {

    // Custom 'between' operator
    public static final ComparisonOperator BETWEEN =
        new ComparisonOperator("=between=", true);

    // Custom 'like' operator with case insensitivity
    public static final ComparisonOperator ILIKE =
        new ComparisonOperator("=ilike=", false);

    public static Set<ComparisonOperator> getOperators() {
        Set<ComparisonOperator> operators = RSQLOperators.defaultOperators();
        operators.add(BETWEEN);
        operators.add(ILIKE);
        return operators;
    }
}

// Usage in parser
RSQLParser parser = new RSQLParser(CustomOperators.getOperators());

Error Handling

Implement robust error handling for invalid RSQL queries:

package com.gonnect.graphql.rsql;

import cz.jirutka.rsql.parser.RSQLParserException;
import graphql.GraphQLException;

public class RsqlQueryHandler {

    public static <T> Specification<T> parseQuery(String rsqlQuery,
            RSQLVisitor<Specification<T>, Void> visitor) {

        if (rsqlQuery == null || rsqlQuery.trim().isEmpty()) {
            return Specification.where(null);
        }

        try {
            RSQLParser parser = new RSQLParser();
            Node rootNode = parser.parse(rsqlQuery);
            return rootNode.accept(visitor);
        } catch (RSQLParserException e) {
            throw new GraphQLException(
                "Invalid filter syntax: " + e.getMessage()
            );
        } catch (IllegalArgumentException e) {
            throw new GraphQLException(
                "Invalid filter field or value: " + e.getMessage()
            );
        }
    }
}

Pagination Support

Combine RSQL filtering with pagination:

package com.gonnect.graphql.resolvers;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

@Component
public class PetQueryResolver implements GraphQLQueryResolver {

    public PetConnection pets(String filter, Integer first,
                              String after, String sortBy) {

        Specification<Pet> spec = PetBuilder.buildSpecification(filter);

        int page = decodeAfterCursor(after);
        int size = first != null ? first : 10;

        Sort sort = sortBy != null
            ? Sort.by(Sort.Direction.ASC, sortBy)
            : Sort.unsorted();

        PageRequest pageRequest = PageRequest.of(page, size, sort);
        Page<Pet> petPage = petRepository.findAll(spec, pageRequest);

        return new PetConnection(petPage);
    }
}

Benefits of GraphQL + RSQL Integration

BenefitDescription
Dynamic FilteringClients can construct complex queries without backend changes
Type SafetyGraphQL schema ensures type-safe responses
Single EndpointAll filtering through one GraphQL endpoint
Reduced API SurfaceNo need for multiple filter parameters
SQL-like SyntaxFamiliar query language for developers
Composable QueriesAND/OR logic for complex conditions

Best Practices

  1. Validate Input: Always validate RSQL queries before execution to prevent injection attacks
  2. Limit Complexity: Consider limiting query depth or complexity to prevent resource exhaustion
  3. Index Fields: Ensure frequently filtered fields are properly indexed
  4. Document Operators: Provide clear documentation of supported RSQL operators
  5. Handle Errors Gracefully: Return meaningful error messages for invalid queries

Conclusion

Integrating RSQL with GraphQL provides a powerful combination that gives clients maximum flexibility while maintaining the benefits of GraphQL's type system and efficient data fetching. This approach is particularly valuable for:

  • Admin dashboards requiring ad-hoc filtering
  • Search functionality with complex criteria
  • Reporting systems with dynamic queries
  • APIs serving diverse client needs

The graphql-rsql project demonstrates how these technologies work together seamlessly, using Spring Boot's ecosystem to build production-ready APIs with sophisticated query capabilities.


Further Reading