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:
-
UI over-enforced: The "cannot remove last item" behavior applied to ALL required array
props, even those wherevalue: []is perfectly schema-valid. -
API under-enforced: Programmatic saving accepted
value: []for required
array props (correct per spec, but inconsistent with UI behavior). See
MR !923 note_749762. -
minItems: 1was blocked entirely: Canvas rejected all SDC and code
component props that includedminItems, making it impossible to explicitly opt into
≥1 enforcement.
Overview
Related: #3529788: Required `type: string` (without other constraints, so "prose strings") props should fall back to empty string (even though invalid) while Content Author is typing, to allow matching live preview in most cases and #3537154: Regression in #3529788: can publish component instances with empty required props
#3529788: Required `type: string` (without other constraints, so "prose strings") props should fall back to empty string (even though invalid) while Content Author is typing, to allow matching live preview in most cases introduced graceful degradation for required string props (retaining empty string during
editing) with a @todo Expand to support multiple-cardinality. #3537154: Regression in #3529788: can publish component instances with empty required props 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
minItemsvalues in bothJsonSchemaType.phpand
EntityFieldPropSourceMatcher.php(treating it as redundant withrequired). -
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: 1is the correct JSON Schema mechanism to require ≥1 array item. -
Drupal Field API's
setRequired(TRUE)means "≥1 value" — which maps to
minItems: 1semantics, not barerequired. -
The UI enforcement chain: PHP
setRequired(TRUE)→ Drupal renders
.form-requiredCSS class → JSisRemoveButtonEnabled()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) | Canvas-ineligible |
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 allowsminItems
when value is exactly1; 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— addedminItems?: numberto both
CodeComponentPropandCodeComponentPropSerialized. -
ui/src/features/code-editor/codeEditorSlice.ts—toggleRequirednow
auto-setsminItems: 1when enabling required on an array prop, and clears it when
disabling. -
ui/src/features/code-editor/utils/utils.ts—serializePropsoutputs
minItemsanddeserializePropsreads 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:
sparkline—required: [data], nominItems;[]is schema-valid (Canvas-eligible)sparkline_min_1—required: [data]+minItems: 1;[]fails validation (Canvas-eligible)sparkline_min_2—minItems: 2; Canvas-ineligible
New/updated tests in SingleDirectoryComponentTest:
-
testValidateComponentInputRejectsEmptyRequiredMultiCardinalityPropWithMinItems1:
verifiesvalue: []produces ≥1 violations, andvalue: [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.
Issue fork canvas-3516754
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
wim leersI didn't grok this issue summary at first, but by reading https://git.drupalcode.org/project/experience_builder/-/merge_requests/8... too, I did grok it. Added some clarifications.
Related original MR thread: https://git.drupalcode.org/project/experience_builder/-/merge_requests/8...
Comment #3
wim leers#3467870: Support `{type: array, …}` prop shapes landed. But, it's impossible right now to close this issue, because per #3467870-28: Support `{type: array, …}` prop shapes, selecting multiple values doesn't quite work yet.
Comment #4
wim leersRelated:
Comment #6
tedbowThis could be worked on now correct?
#3546869: Editing support for `type: array`: support multiple-cardinality Field Widgets (`StaticPropSource`s) stops the editing but we could still write back tests to confirm whatever approach we decide on works
Comment #7
vipin.mittal18Comment #8
narendrarThis code is already removed in #3529788: Required `type: string` (without other constraints, so "prose strings") props should fall back to empty string (even though invalid) while Content Author is typing, to allow matching live preview in most cases. If this is still a problem, it would be helpful if someone can please add the Steps to reproduce 🙏.
Comment #9
narendrarComment #10
vipin.mittal18Hello @larowlan, It seems this issue is outdated.
Comment #11
wim leersIt's still needed.
#3529788: Required `type: string` (without other constraints, so "prose strings") props should fall back to empty string (even though invalid) while Content Author is typing, to allow matching live preview in most cases introduced
It also introduced a regression, which was fixed in #3537154: Regression in #3529788: can publish component instances with empty required props, and then adde3d another
@todo:Both of those need to be fixed here.
Comment #13
wim leersDiscussed MR713 with @tedbow. Approved his test additions on the MR and provided a pointer.
However, that actually is only tangentially related to the original intent/scope of this issue.
type: array) props. See IS + #11.Comment #14
tedbowFYI, I have pushed up https://git.drupalcode.org/issue/canvas-3516754/-/tree/3516754-test-module which contains a test module that allows editing auto-save directly. using it for debugging locally I think because other multi-value issues are still in motion and I tried emptying out an array field from the UI and was not able to.
Comment #15
tedbowSince #11 the code @wim-leers referenced has changed in #3546869: Editing support for `type: array`: support multiple-cardinality Field Widgets (`StaticPropSource`s) again and
Was removed.
Now the code is
Also in that issue
\Drupal\canvas\PropSource\StaticPropSource::isEmptywas also addedand test coverage was added to
\Drupal\Tests\canvas\Kernel\Plugin\Canvas\ComponentSource\SingleDirectoryComponentTest, specificallytestValidateComponentInputFiltersEmptyItemsForMultiCardinalityPropsandtestClientModelToInputRetainsEmptyArrayForRequiredMultiCardinalityPropI am still looking this to see if the current is still relavent or if need to add more test covergae
Comment #17
tedbowSo I pushed up another MR. @wim-leers I am not sure if will agree this relates to the scope of this issue but I do think it relates to the title "Review how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty"
but the issue summary says
This would imply this issue is only dealing with the authoring experience so I am not sure this mean to cover this but..
Basically I found that programmatically you can save required prop of the type array with an empty array \Drupal\Tests\canvas\Kernel\Plugin\Field\FieldType\ComponentTreeItemListTest::testEntityLevelValidationAllowsEmptyRequiredMultiCardinalityProp
requiredin json schema terms just means the key is test not that the array can't be empty. You could enforce it withminItems: 1but we don't allow that. But it seems from our code comment we don't allowminItems: 1because we assume `required` would enforce the same rule, which is does not.We do enforce array props that are required in the UI. I have figured this out through manual testing. I think we should enforce the rules on the API level too, which we currently are not. Or least document that we aren't
See my MR comments and code comments in the MR
Comment #18
wim leers#17: I think it definitely relates. That's exactly why we have this issue!
Comment #19
tedbowComment #20
tedbowsummary update to reflect the current state of !923 and the fact that todos mentioned have actually been removed
I pretty sure this will fail tests be will see
Comment #21
tedbowLooking into phpunit test failures first
Comment #22
tedbow@isholgueras thanks for the review, I have addressed you feedback
Comment #23
wim leers@tedbow asked me to review this.
I had one key concern (see link for details, summarized: .
Then I saw that this MR is updating the code component editor UI to ensure that whenever a prop is marked as required,
minItems: 1is also set. That's exactly what I proposed!But I proposed to do this generically: to impose this at the prop-definition-in-JSON-Schema level, i.e. for both code components and SDCs. For code components, this is already making it so. For SDCs, it is not. That's easily solved.
So:
minItems: 1and nominItems. If atype: arrayprop is required withoutminItems: 1, then it should simply be optional!type: arrayprops need an update pathIn my proposal:
Behavioral Changes
required: [prop], nominItems,value: []required: [prop]+minItems: 1,value: []minItems: 2minItemsserializedminItems: 1auto-serialized 👈 Unchanged, this makes sense, but it doesn't make sense that SDCs were treated differently ⚠️ However, this AFAICT means this will need an update path for code components that have already been created.Summarized my changes on the MR: https://git.drupalcode.org/project/canvas/-/merge_requests/923#note_811732
Comment #24
wim leersPer https://git.drupalcode.org/project/canvas/-/merge_requests/923#note_811745, #3572553: Save Multi-Value Prop Configuration for Code Components should land first.
My concerns in #23 may seem big, but they're not. They're essentially just: "your analysis is right, your solution is right, but you went too far in supporting what does not make sense; we should instead require the sensible thing you applied to code components to everything".
So: fantastic work here! 🤩👏 It resolves a piece of technical debt that I introduced in #3467870: Support `{type: array, …}` prop shapes (I did not handle the "empty" edge case at all in there).
This MR even significantly improves shape matching against (required) multi-value entity fields, resulting in more structured data becoming available in Canvas! 💙
Comment #25
wim leersPushed several more "update tests" commits. I think they illustrate how the slight shift in #23 significantly improves cohesion and reduces complexity.
The only remaining test failures are those in
JavaScriptComponent(Validation)Test+JsComponentTest, which illustrate the need for an update path.I'll leave that for @tedbow & co to address.
Comment #27
wim leersPaired with @tedbow on this issue/MR.
He had an insight I was missing: all of the "multi-value prop" functionality was gated behind the
canvas_dev_modemodule. Hence no code components could've been created that would need an update path!(The only exceptions could be: code components could've used
type: arraysince #3568264, which shipped in https://www.drupal.org/project/canvas/releases/1.3.0 on March 21. We'll work with @balintbrews to figure out if somebody could've conceivably already used it, and if so, we'll add an update path in a subsequent MR. To be clear: nobody could have created this through the Canvas UI! That's why #3577946: [PP-2] Remove Canvas Dev Mode flag for Multi-Value Props support exists: to actually enable this for everyone. And that's why this issue blocks that one.)Comment #28
wim leersCR pair-written: https://www.drupal.org/node/3587384.
Comment #30
tedbowThanks, every one!
Comment #32
nagwani commented