Skip to content

Filtering strategy

Summary

Problem statement

The CommonGrants protocol needs a standardized approach to filtering across endpoints that balances consistency, flexibility, and ease of implementation.

Sub-questions

  • Should filters be passed in the request body or as query parameters?
  • If the request body accepts a filters parameter, should it be an object or an array?
  • Which filter operators (e.g. eq, neq, gt, etc.) should be supported?
  • How should the protocol support implementation-defined filters?

Decision drivers

  • Consistency: Filters should be consistent across endpoints.
  • Flexibility: Filters should support complex queries while keeping implementation straightforward.
  • Compatibility: Filters should be easy to combine with pagination and sorting.
  • Extensibility: Filters should provide a way to support implementation-defined filters.

Decision outcome

API endpoints that support filtering should be POST operations that accept a filters parameter in the request body. Filters will be defined using a standard schema to ensure consistency across different endpoints.

Filtering approach

  • The filters parameter MUST be included at the root of the request body.
  • Each filter MUST conform to the Filter schema, which contains:
PropertyTypeRequiredDescription
operationenumYesThe operation to perform on the value
valueanyYesThe data to use for the filter operation
  • Supported operations:
OperationDescriptionSupported value types
eqEqual tostring, number, boolean, date
neqNot equal tostring, number, boolean, date
gtGreater thannumber, date
gteGreater than or equal tonumber, date
ltLess thannumber, date
lteLess than or equal tonumber, date
likeContainsstring
inIn listarray
not_inNot in listarray
betweenBetween two valuesrange object with min and max properties

Custom filters

  • If a route supports implementation-defined filters, it MUST include a customFilters property.
  • This property must be an object whose values match the Filter schema.
  • Unsupported custom filters should be ignored and reported in the filterInfo.errors property.

Examples

An example of a request body that includes a standard filter:

{
"filters": {
"title": {
"value": "example",
"operation": "like"
},
"closedDateRange": {
"value": {
"min": "2024-01-01",
"max": "2024-01-31"
},
"operation": "between"
}
}
}

An example of a request body that includes both standard and custom filters:

{
"filters": {
"title": {
"value": "example",
"operation": "like"
},
"customFilters": {
"agency": {
"value": ["Department of Transportation"],
"operation": "in"
}
}
}
}

Consequences

  • Positive
    • Establishes a consistent pattern for filtering across endpoints.
    • Supports more complex filters than query parameters.
    • Provide a consistent way to extend the protocol with implementation-defined filters.
  • Negative
    • Passing filters in the request body makes it harder to cache requests.
    • Using an object to represent filters makes it harder to support complex queries that include multiple filters with the same property name.

Options considered

These are the other options we considered but did not choose.

Query parameters only

  • Pros
    • Easy to implement for simple filters.
    • Fully cacheable in HTTP caches.
  • Cons
    • Difficult to support complex filtering.
    • Doesn’t provide a way to support implementation-defined filters.

A filter request body parameter of type array

  • Pros
    • Easy to understand.
    • Supports complex filtering with multiple filters for the same property.
    • Could support index-based filter logic (e.g. (1 AND 2) OR (3 AND 4)).
  • Cons
    • Doesn’t provide a way to support implementation-defined filters.
    • Makes it harder to define standard and custom filters in the OpenAPI document.
    • Makes it harder for API servers to parse request bodies in a deterministic way.