Skip to content

feat: add ObjectValidator for hash/schema validation#25

Merged
danielnottingham merged 2 commits into
mainfrom
feat/object_hash_validator
Apr 20, 2026
Merged

feat: add ObjectValidator for hash/schema validation#25
danielnottingham merged 2 commits into
mainfrom
feat/object_hash_validator

Conversation

@danielnottingham
Copy link
Copy Markdown
Owner

Description

Adds ObjectValidator, a schema-based validator for Hash values, completing the most-requested feature from the roadmap. Defined via ValidatorRb.object(schema), it validates each field through a sub-validator and re-emits errors with a composed key path (e.g. path: [:address, :zip]), which also enables arbitrary nesting of objects inside objects and objects inside arrays.

String and symbol keys are interchangeable between schema and input — a common source of friction when validating payloads that cross JSON and Ruby boundaries. The API ships with Zod-style schema modifiers: .strict (rejects undeclared keys with :unknown_key), .partial
(treats every key as optional), and .pick / .omit (derive a narrower schema). All three modifiers return a new validator instance, so a single base schema can be reused safely for login, update, and public-view variants without cross-contamination.

Fixes #9

Type of Change

  • New feature (non-breaking change which adds functionality)

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the CHANGELOG.md file
  • Any dependent changes have been merged and published

Test Coverage

Added spec/validator_rb/object_validator_spec.rb with 26 examples covering: hash/non-hash inputs, symbol↔string key interop, :required reporting on missing keys, nested path composition, .strict / .partial / .pick / .omit semantics, and immutability of derived
validators. Full suite: 192 examples, 0 failures, 100% line coverage, 0 RuboCop offenses (bundle exec rspec && bundle exec rubocop).

it "builds a path across nesting levels" do               
  validator = ValidatorRb.object(                                                                                                                                                                                                                                                     
    address: ValidatorRb.object(                                                                                                                                                                                                                                                      
      zip: ValidatorRb.string.regex(/\A\d{5}\z/).optional
    )                                                                                                                                                                                                                                                                                 
  )                                                       
                                                                                                                                                                                                                                                                                      
  result = validator.validate(address: { zip: "abc" })    
                                                                                                                                                                                                                                                                                      
  expect(result.errors.first.path).to eq(%i[address zip])
  expect(result.errors.first.code).to eq(:invalid_format)                                                                                                                                                                                                                             
end                                                       
                                                                                                                                                                                                                                                                                      
Additional Notes                                          

- .partial treats missing keys as valid but still runs the sub-validator when the key is present (even with nil). This is intentional: it keeps the "optional" semantics explicit and avoids silently mutating user-provided sub-validators.                                          
- .strict, to stay consistent with the existing .required / .optional modifiers in BaseValidator, mutates the receiver. .partial / .pick / .omit do not  they return fresh instances because they reshape the schema.
- Docs (README.md, CHANGELOG.md) and CLAUDE.md will land in a follow-up  flagged in the checklist above.                                                                                                                                                                             
- Out of scope (can be follow-ups): a result.errors_by_path helper that reshapes the flat error list into the nested-hash format shown in the issue, and thread-safety notes for schema definition.

  Introduces ValidatorRb.object(schema) for validating hashes against a
  map of field-level sub-validators, addressing #9.

  - Accepts symbol or string keys interchangeably between schema and input
  - Emits sub-validator errors with a composed key path (e.g. [:address, :zip])
  - .strict rejects undeclared keys with :unknown_key
  - .partial / .pick / .omit return new instances, preserving flags
  - Nests arbitrarily (ObjectValidator inside ObjectValidator)
@danielnottingham danielnottingham self-assigned this Apr 20, 2026
  - README: adds Object Validators section covering schema basics, nested
    error paths, and the strict/partial/pick/omit modifiers; drops hash
    from the roadmap
  - CHANGELOG: logs ObjectValidator under [Unreleased] with its schema
    shape, key interop, composed path, and modifier semantics
  - CLAUDE: adds ObjectValidator to the validator list, documents the
    multi-error validation pattern (push directly to @validations +
    flatten) used by ArrayValidator#of and the schema block, and the
    nested-path composition shared across container validators
@danielnottingham danielnottingham merged commit 8b553a0 into main Apr 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Object/Hash Validator

1 participant