JSON Mask for Ruby
json-mask selects fields from JSON-compatible Ruby objects while preserving the shape of the
response. It implements the field selector language used by Google's partial responses and the
JSON Mask project.
The library has no runtime dependencies.
Installation
Add the gem to your Gemfile:
gem "json-mask"
Then run bundle install.
Usage
require "json_mask"
response = {
"id" => "abc123",
"name" => "Product demo",
"permissions" => [
{"id" => "owner", "role" => "owner", "email" => "owner@example.com"}
]
}
JsonMask.call(response, "id,permissions(id,role)")
# => {
# "id" => "abc123",
# "permissions" => [{"id" => "owner", "role" => "owner"}]
# }
JsonMask.mask is an alias for JsonMask.call.
Compile selectors that will be reused:
mask = JsonMask.compile("id,name,permissions(role)")
mask.call(first_response)
mask.call(second_response)
Compiled masks are immutable and safe to share between threads.
Passing nil, an empty string, or a whitespace-only string returns the original value unchanged.
This makes an optional HTTP fields parameter straightforward:
render json: JsonMask.call(payload, params[:fields])
Selector syntax
The syntax is loosely based on XPath:
| Selector | Meaning |
|---|---|
id,name |
Select multiple fields |
permissions/role |
Select a nested field |
permissions(id,role) |
Select multiple fields from an object or each object in an array |
permissions/* |
Select every field below permissions |
items/*/id |
Select id from every value below items |
Slash paths and parenthesized sub-selections traverse arrays transparently. Empty hashes remain in arrays, preserving their positions. Missing fields are omitted.
Backslash escapes structural characters in field names:
JsonMask.call({"a/b" => 1, "other" => 2}, 'a\/b')
# => {"a/b" => 1}
JsonMask.call({"*" => 1, "other" => 2}, '\\*')
# => {"*" => 1}
The structural characters are ,, /, (, ), *, and \\. An asterisk is a wildcard only
when it is the entire, unescaped field name. Unescaped whitespace around field names and operators
is ignored; whitespace inside a field name is preserved.
String and symbol hash keys are supported, and the result preserves the key objects from the input. The input is never mutated.
Invalid selectors and limits
Malformed selectors raise JsonMask::ParseError, which includes the original expression and the
zero-based character offset:
JsonMask.compile("files(id,,name)")
# raises JsonMask::ParseError: expected a field name at offset 9
The parser applies conservative defaults suitable for accepting selectors from HTTP or MCP clients:
- Maximum selector length: 16,384 bytes
- Maximum nesting depth: 64
- Maximum field selectors: 1,000
The limits can be tightened for a specific boundary:
JsonMask.compile(fields, max_length: 1_024, max_depth: 16, max_selectors: 100)
Exceeding a limit raises JsonMask::LimitError, a subclass of JsonMask::ParseError.
Validation is syntactic. Because the library has no response schema, a well-formed selector that names a field absent from the input simply omits that field; it cannot produce Google's schema-aware "Invalid field selection" error on its own.
Compatibility
The supported grammar follows the Google Drive fields parameter rules
and JSON Mask's documented grammar. This library intentionally validates malformed expressions
instead of attempting to recover from them.
The projector accepts JSON-compatible Hash and Array values. If a selected field contains a
scalar where the selector asks for nested fields, that field is omitted — except nil, which
passes through unchanged (matching the reference implementation), so a nullable field stays
distinguishable from an unselected one. A non-container root value with a non-empty selector
produces nil.
Development
bundle install
bundle exec rake
bundle exec rake build
The default Rake task runs the full test suite and RuboCop.
License
MIT. See LICENSE.txt.