Skip to content

Translate ValidationErrors#full_messages once instead of on every call - #2934

Closed
ericproulx wants to merge 1 commit into
masterfrom
perf/memoize-validation-full-messages
Closed

Translate ValidationErrors#full_messages once instead of on every call#2934
ericproulx wants to merge 1 commit into
masterfrom
perf/memoize-validation-full-messages

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

ValidationErrors builds its #message from #full_messages when it is created. #full_messages then translated every error again on each call: an I18n lookup for the grape.errors.format string (with interpolation) and one per attribute name. Each is a few microseconds on I18n 1.15.

The README's recipe for answering with the list, below, therefore paid for every translation twice:

rescue_from Grape::Exceptions::ValidationErrors do |e|
  error!({ messages: e.full_messages }, 400)
end

The list is now kept from the first call and handed out as a copy, so a caller changing it cannot change what the next caller gets. #message is still built through #full_messages, so a subclass overriding it keeps working as before.

Benchmarks

Median of 7 interleaved subprocess rounds, Ruby 4.0.6. The API uses the README handler above, with requires :id, type: Integer and requires :name, type: String.

request no JIT YJIT
one param missing +21.9% +24.9%
both params missing +27.3% +34.4%
default handler, missing param −0.1%
default handler, invalid Integer −0.1%

Behaviour

The list now stays what #message was built from. Before, a handler that switched the locale and then asked for the list got a mix of both locales. With a French format and a French presence message, I18n.with_locale(:fr) { e.full_messages } answered ["id : is missing", "nick : is missing"] for an error raised in English: the French format and untranslated attribute names around the English messages, which were translated when each Validation was raised. It now answers ["id is missing", "Nickname is missing"], the same text as #message. I did not add an UPGRADING entry, since the old output was half-translated anyway; happy to add one.

Otherwise responses are byte-identical to master over:

  • a 120-request matrix of rescue_from ValidationErrors handlers reading full_messages (once, twice, and after changing the list), message, both together, as_json, to_json, errors, error!(e), and re-raising. It includes translated attribute names, a request in another locale, mutually_exclusive (several params in one error) and values. The locale-switching handler above is the only difference.
  • the 228-case error-path matrix and the 104-case and 108-case validation matrices from earlier PRs.

Missing spec, added

A caller changing the array #full_messages returned must not change the next answer. The spec passes on master, where each call builds a new array, and fails if the copy is dropped.

Test plan

  • Full RSpec suite passes locally.
  • RuboCop clean.
  • Mutation-checked: dropping the copy fails the new spec. Dropping the memo is invisible by design, and the benchmark covers it.
  • CI green.

🤖 Generated with Claude Code

ValidationErrors builds its #message from #full_messages when it is
created, and #full_messages translated every error again on each call:
an I18n lookup for the format and one per attribute name, a few
microseconds apiece. The README's recipe for answering with the list,

    rescue_from Grape::Exceptions::ValidationErrors do |e|
      error!({ messages: e.full_messages }, 400)
    end

therefore paid for every translation twice. The list is now kept from
the first call and handed out as a copy, so a caller changing it cannot
change what the next caller gets; a spec pins that.

The list now stays what #message was built from. Before, a handler that
switched the locale and then asked for it got the format and attribute
names in the new locale around messages still in the old one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the perf/memoize-validation-full-messages branch from 66cae11 to 08245fd Compare September 11, 2026 09:36
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Danger Report

Errors

  • One of the lines below found in CHANGELOG.md doesn't match the expected format. Please make it look like the other lines, pay attention to version numbers, periods, spaces and date formats.

Markdowns

* [#XXXX](https://github.com/ruby-grape/grape/pull/XXXX): Translate `ValidationErrors#full_messages` once instead of on every call - [@ericproulx](https://github.com/ericproulx).
does not include a pull request link

View run

@ericproulx

Copy link
Copy Markdown
Contributor Author

Closing in favour of #2936, which combines #2922#2934 into one PR, re-benchmarked as a whole against master. The write-up here (behaviour matrix, mutation results) still describes this part of the change.

@ericproulx ericproulx closed this Sep 11, 2026
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.

1 participant