Skip to content

Guard array fields against required: + default: - #71

Merged
VSN2015 merged 1 commit into
masterfrom
fix/array-required-default-guard
Sep 27, 2026
Merged

VSN2015 merged 1 commit into
masterfrom
fix/array-required-default-guard

Conversation

@VSN2015

@VSN2015 VSN2015 commented Sep 25, 2026

Copy link
Copy Markdown
Owner

The bug

array fields had no guard against declaring both required: true and default: — the two are semantically contradictory, since a default only ever applies when a field is absent, and a required field can never be absent by definition without erroring first. validate_scalar_opts! and validate_json_opts! already raise on this combination at class load, but array had no equivalent check, so this loaded silently:

permit_params(:create) do
  array :tags, of: :string, required: true, default: ["x"]  # no error at class load
end

At request time, permittable_check_hash checks field.key?(:default) before field[:required], so an absent tags is quietly treated as optional-with-default — required: true becomes a no-op. Worse, lib/permittable/json_schema.rb builds the exported "required" array purely from field[:required], with no awareness of default:, so the published JSON Schema says tags is required while the real contract happily accepts a request that omits it — a genuine behavior/schema divergence, not just cosmetic.

Nested required(:name) { ... } fields were never affected: NESTED_OPTS doesn't even list :default as an allowed option, so assert_opts! already rejects default: there at class load via a different mechanism. This bug — and this fix — is scoped to array fields only.

The fix

Add the same required+default guard validate_scalar_opts!/validate_json_opts! already carry, to array:

field = { name: name, kind: :array, required: required, **opts }
if field[:required] && field.key?(:default)
  raise ArgumentError, "#{LABEL}: field :#{name} is required and cannot have a :default (default implies optional)"
end

Now the same declaration fails loudly at class load, the same way the scalar and JSON cases already do:

ArgumentError: Permittable: field :tags is required and cannot have a :default (default implies optional)

Verification

  • bundle exec rspec — 849 examples, 0 failures
  • bundle exec rubocop lib/permittable.rb spec/permittable_spec.rb — no offenses
  • New test written first (spec/permittable_spec.rb), confirmed to fail before the implementation change and pass after

🤖 Generated with Claude Code

The same required+default contradiction validate_scalar_opts! and
validate_json_opts! already reject at class load was missing from
array. required: true was silently a no-op on an array field with a
default:, and the exported JSON Schema said the field was required
while the real contract accepted a request that omitted it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@VSN2015
VSN2015 merged commit aa95288 into master Sep 27, 2026
16 checks passed
VSN2015 pushed a commit that referenced this pull request Sep 27, 2026
Bumps version.rb, retitles the Unreleased CHANGELOG section, and includes the
regenerated Gemfile.lock — CI runs bundler in frozen mode, so a version bump
without the lockfile fails the tag build.

Twenty PRs since 0.8.0 (#16, #35, #51-#57, #59-#66, #71-#73): Rails 8
params.expect support and model-aware drafting in permittable:generate, the
permittable:audit coverage command, reusable field groups
(Permittable.fields/use), accept_params/reject_params RSpec matchers, RFC
9457 problem+json, named format: presets, and a type-checking mode for the
schema-drift guard — plus a from-scratch Ruby-to-ECMA-262 pattern
translator, a correctness overhaul of in: list casting and authored
default:/example: storage, several encoding-crash and log-forging fixes, and
three freshly-found gaps: an array field's required: + default: silently
behaved as optional, accept_params/reject_params disagreed with a standalone
Contract's own unknown: strictness, and the generator could draft a field
from a permit call that only existed inside a log string.

Minor rather than patch: mostly new surface, but a :string field's in: list
of Symbols now matches correctly where it used to reject every request, and
an array field combining required: and default: now fails at class load
instead of silently treating the field as optional. See CHANGELOG.md for the
complete list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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