#3516754 Require `minItems: 1` for required `type: array` props

Problem Summary

This issue addresses a semantic mismatch in how Canvas treats "required" for type: array (multi-cardinality) props. Canvas was incorrectly treating required: [prop] as equivalent to minItems: 1, which is wrong per JSON Schema. This caused three separate problems:

  1. UI over-enforced: The "cannot remove last item" behavior applied to ALL required array props, even those where value: [] is perfectly schema-valid.
  2. API under-enforced: Programmatic saving accepted value: [] for required array props (correct per spec, but inconsistent with UI behavior). See MR !923 note_749762.
  3. minItems: 1 was blocked entirely: Canvas rejected all SDC and code component props that included minItems, making it impossible to explicitly opt into ≥1 enforcement.

Overview

[#3529788] introduced graceful degradation for required string props (retaining empty string during editing) with a @todo Expand to support multiple-cardinality. [#3537154] fixed a regression and added a second such @todo. Both of those @todo items have since been addressed and removed from the codebase, making this issue the remaining piece of the multi-cardinality required-prop story.

Root Cause

The false assumption was encoded directly in a code comment in JsonSchemaType.php:

// marking an SDC prop as required has the same effect as `minItems: 1`

This incorrect belief led Canvas to:

  • Block all minItems values in both JsonSchemaType.php and EntityFieldPropSourceMatcher.php (treating it as redundant with required).
  • Call setRequired(TRUE) on the Drupal field for ALL required array props — causing the form to show the required asterisk and prevent removing the last item, even when the JSON Schema did not require it.

This assumption is false. Proven by tests in MR !923 note_749761: with required: ['data'] but no minItems, storing value: [] produces zero violations from validateComponentInput().

Key Technical Findings

  • required: [prop] in JSON Schema means "the key must be present in the object" — it does NOT mean the array must be non-empty. value: [] is perfectly schema-valid.
  • minItems: 1 is the correct JSON Schema mechanism to require ≥1 array item.
  • Drupal Field API's setRequired(TRUE) means "≥1 value" — which maps to minItems: 1 semantics, not bare required.
  • The UI enforcement chain: PHP setRequired(TRUE) → Drupal renders .form-required CSS class → JS isRemoveButtonEnabled() checks .form-required + rowCount === 1 → remove button disabled.

Behavioral Changes

Scenario

Before fix

After fix

SDC with required: [prop], no minItems, value: []

UI blocks removing last item (incorrect)

UI allows removing all items (correct per JSON Schema)

SDC with required: [prop] + minItems: 1, value: []

Canvas-ineligible (all minItems blocked)

Canvas-eligible; [] correctly fails validation

SDC with minItems: 2

Canvas-ineligible

Still ineligible (Drupal Field API cannot enforce minimum cardinality > 1)

Code component editor: toggle Required ON for array prop

No minItems serialized

minItems: 1 auto-serialized

Proposed resolution

1 — Allow minItems: 1 in prop definitions (PHP)

Two files previously blocked ALL minItems values:

  • src/JsonSchemaInterpreter/JsonSchemaType.php — now allows minItems when value is exactly 1; rejects all other values. Drupal Field API has no concept of minimum cardinality > 1.
  • src/ShapeMatcher/EntityFieldPropSourceMatcher.php — same change.

Also removed the false code comment claiming required and minItems: 1 are equivalent.

2 — Code component editor auto-sets minItems: 1 (TypeScript)

Component authors using the Canvas code editor should not need to know about minItems: 1 — toggling "Required" on an array prop is enough.

  • ui/src/types/CodeComponent.ts — added minItems?: number to both CodeComponentProp and CodeComponentPropSerialized.
  • ui/src/features/code-editor/codeEditorSlice.tstoggleRequired now auto-sets minItems: 1 when enabling required on an array prop, and clears it when disabling.
  • ui/src/features/code-editor/utils/utils.tsserializeProps outputs minItems and deserializeProps reads it back.

3 — Fix UI enforcement (PHP)

src/Plugin/Canvas/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php: setRequired(TRUE) is now only called for array props when BOTH required AND minItems: 1 are present. For array props with required but no minItems: 1, setRequired(FALSE) is used — no required asterisk, user CAN remove all items.

No JavaScript changes were needed. isRemoveButtonEnabled() already correctly read the .form-required CSS class; fixing the PHP source of that class was sufficient.

New test SDC: sparkline_min_1

tests/modules/canvas_test_sdc/components/sparkline_min_1/ — a Canvas-eligible sparkline with explicit minItems: 1. This forms a three-component test set:

  • sparklinerequired: [data], no minItems; [] is schema-valid (Canvas-eligible)
  • sparkline_min_1required: [data] + minItems: 1; [] fails validation (Canvas-eligible)
  • sparkline_min_2minItems: 2; Canvas-ineligible

New/updated tests in SingleDirectoryComponentTest:

  • testValidateComponentInputRejectsEmptyRequiredMultiCardinalityPropWithMinItems1: verifies value: [] produces ≥1 violations, and value: [42] produces 0.
  • testValidateComponentInputAllowsEmptyRequiredMultiCardinalityPropWithNoMinItems: clarified that 0 violations for [] on a bare-required prop IS correct post-fix behavior, not a bug.

Developer experience note

For SDC authors: required: [prop] alone intentionally does NOT enforce ≥1 items in Canvas. This respects JSON Schema semantics and keeps the SDC valid across non-Canvas contexts. To enforce ≥1 value, add minItems: 1 explicitly. minItems > 1 remains unsupported (Drupal Field API cannot enforce it).

For code component authors: Toggling "Required" in the Canvas editor for an array prop automatically handles minItems: 1 — no manual YAML editing needed.

User interface changes

No new UI visible to content authors. The behavior change — required array props without minItems: 1 no longer show the required asterisk or block removing the last item — is a bug fix aligning the UI with JSON Schema semantics, not a new UI feature.

Edited by Ted Bowman

Merge request reports

Loading