JSON Mask for Ruby

CI

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.