Skip to content

Instantiate validators at definition time#2657

Draft
ericproulx wants to merge 1 commit intomasterfrom
revisit_validators
Draft

Instantiate validators at definition time#2657
ericproulx wants to merge 1 commit intomasterfrom
revisit_validators

Conversation

@ericproulx
Copy link
Contributor

@ericproulx ericproulx commented Feb 12, 2026

Summary

Validators are now instantiated once at route definition time rather than per-request via ValidatorFactory. This eliminates repeated object allocation on every request and moves expensive setup (option parsing, converter building, message formatting) out of the hot path.

Because validator instances are shared across all concurrent requests they are frozen after initialization, enforced by a Validators::Base.new override that calls freeze_state! then freeze. A new Grape::Util::DeepFreeze helper recursively freezes Hash/Array/String values while intentionally leaving Procs, coercers, and other mutable objects unfrozen.

Changes

Core

  • ParamsScope#validate instantiates the validator class directly and stores the instance in namespace_stackable[:validations]; removes ValidatorFactory
  • Endpoint#run_validators reads validator instances directly from saved_validations and removes the validations enumerator method
  • ParamsScope is frozen at the end of initialize; element, parent, and Attr#key/Attr#scope changed from attr_accessor to attr_reader
  • coerce_type now receives only the extracted coercion keys so callers don't need to delete them afterwards

Validators::Base

  • Base.new calls private freeze_state! (deep-freezes all ivars) then freeze; subclasses can override freeze_state! to skip specific ivars
  • fail_fast? promoted to explicit public method
  • validate_param! promoted to protected with a NotImplementedError default
  • hash_like? extracted as a private helper

Grape::Util::DeepFreeze

  • New module with a single deep_freeze(obj) function
  • Freezes Hash (keys + values), Array (elements), and String recursively
  • Returns all other types (Proc, Class, coercers, etc.) untouched

Validator-level eager initialization

  • AllowBlankValidator: caches @value and @exception_message
  • CoerceValidator: resolves type and builds converter at definition time; overrides freeze_state! to leave the coercer unfrozen
  • DefaultValidator: pre-builds @default_call lambda
  • ExceptValuesValidator / ValuesValidator: validates proc arity at init
  • RegexpValidator, SameAsValidator, PresenceValidator: cache messages
  • AllOrNoneOf, AtLeastOneOf, MutuallyExclusiveValidator, ExactlyOneOfValidator: cache exception messages
  • ContractScopeValidator: no longer inherits from Base

Specs

  • New DeepFreezeSpec
  • New specs for SameAsValidator and ExceptValuesValidator
  • ContractScopeValidatorSpec / custom_validations_spec updated for the new instantiation model

Test plan

  • bundle exec rspec
  • Verify no regressions in validation behaviour
  • Confirm frozen validator instances raise FrozenError on any attempt to mutate state at request time

@ericproulx ericproulx marked this pull request as draft February 12, 2026 08:42
@github-actions
Copy link

github-actions bot commented Feb 12, 2026

Danger Report

No issues found.

View run

@ericproulx ericproulx force-pushed the revisit_validators branch 2 times, most recently from 5d145a5 to f87920f Compare February 12, 2026 08:49
@ericproulx
Copy link
Contributor Author

Missing UPGRADING notes. Working on it

@ericproulx ericproulx force-pushed the revisit_validators branch 3 times, most recently from 2ecb403 to cf04c9d Compare February 12, 2026 13:04
@dblock
Copy link
Member

dblock commented Feb 12, 2026

Is there a tradeoff with things like @exception_message = message(:all_or_none) being always allocated? can they become class variables?

@ericproulx
Copy link
Contributor Author

Is there a tradeoff with things like @exception_message = message(:all_or_none) being always allocated? can they become class variables?
message involves @option so it can't be a class variable.

@ericproulx ericproulx force-pushed the revisit_validators branch 12 times, most recently from e16efcf to 0b9e34b Compare February 18, 2026 22:27
@ericproulx ericproulx force-pushed the revisit_validators branch 8 times, most recently from a503556 to 2b16ba1 Compare February 23, 2026 11:05
@ericproulx ericproulx force-pushed the revisit_validators branch 7 times, most recently from b744723 to 30b09ea Compare February 28, 2026 17:03
@ericproulx ericproulx force-pushed the revisit_validators branch 14 times, most recently from ff94e8f to 6fcca15 Compare March 12, 2026 09:53
Validators are now instantiated once at route definition time rather than per-request, eliminating repeated allocation overhead. Validator instances are frozen via `DeepFreeze` to make them safe for sharing across requests.

- Override `Validators::Base.new` to call `freeze` after initialization
- Add `Grape::Util::DeepFreeze` to recursively freeze Hash/Array/String values while leaving Procs, scopes, and coercers unfrozen
- Remove `ValidatorFactory` (no longer needed)
- Update all validators to be compatible with frozen instances (move any mutable state out of instance variables set at validation time)
- Add specs for `DeepFreeze`, `SameAsValidator`, and `ExceptValuesValidator`
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.

2 participants