Real cases · 20 of 25
A field an author fills in that visibly does nothing — tracing a dialog's hidden, dead flat fields back to a multifield migration that never cleaned up after itself, and a template-contract test that would have caught it.
Background reading: Dialog Show/Hide Widgets and Conditional Multifields covers the mechanics this post assumes.
An author working on a legacy product-tile template opened the component dialog, found an "Image Path" field and an "Alt Text" field sitting near the top (unlabeled as anything special), filled them in expecting them to set the tile's image, and saved. Nothing changed on the published page. She tried again with a different asset path. Still nothing. She filed it as "the dialog is broken" — no error, no console warning, just a field that visibly accepts input and has zero effect.
Support escalated it as a rendering bug. The actual cause was two dialog generations coexisting in the same content structure: a mediaItems multifield (the current, working mechanism) and a pair of flat imagePath/altText fields left over from before the multifield existed — marked sling:hideResource="{Boolean}true" during the migration instead of deleted, and never actually removed. The fields the author found weren't hidden in her dialog view (a downstream cleanup task had accidentally un-hidden them while "simplifying" the dialog, not realizing what they were), but the Sling Model backing the component had no code path reading ./imagePath or ./altText at all anymore — only the multifield's per-item resources.
Two separate problems needed fixing, and conflating them would have led to the wrong fix:
The fix intentionally does not try to make the old fields "work again" by wiring them back into the model — the multifield is the correct, current mechanism (supports multiple media items, which the flat fields never could), and reviving the flat fields would just recreate two parallel, conflicting ways to set the same visual result.
class MediaTileDialogContractTest {
// Reads the same content.xml the component actually ships, the same way
// AEM's dialog merger would encounter it — not a copy/paste of its text.
private static Document loadDialog() throws Exception {
Path dialogPath = Path.of(
"src/main/content/jcr_root/apps/myapp/components/mediatile/_cq_dialog/.content.xml");
return DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(dialogPath.toFile());
}
@Test
void everyDialogFieldNameHasACorrespondingModelProperty() throws Exception {
Set<String> dialogFieldNames = collectFieldNames(loadDialog());
Set<String> modelReadProperties = ModelPropertyScanner.scan(MediaTileImpl.class);
Set<String> orphanedFields = new HashSet<>(dialogFieldNames);
orphanedFields.removeAll(modelReadProperties);
assertThat(orphanedFields)
.as("dialog fields with no corresponding model property read - likely dead")
.isEmpty();
}
@Test
void hiddenFieldsWithoutAResourceSuperTypeAreTreatedAsDeadNotMerged() throws Exception {
Document dialog = loadDialog();
boolean hasSuperType = dialog.getDocumentElement().hasAttribute("sling:resourceSuperType");
List<String> hiddenFieldNames = collectHiddenFieldNames(dialog);
// A component with no resourceSuperType has nothing to merge against,
// so a sling:hideResource field here can only be a leftover, never
// a legitimate inherited-field suppression.
if (!hasSuperType) {
assertThat(hiddenFieldNames)
.as("hidden fields on a component with no super type - dead leftovers, should be deleted")
.isEmpty();
}
}
@Test
void multifieldItemFieldsAreTheOnlyMediaFieldsPresent() throws Exception {
Set<String> dialogFieldNames = collectFieldNames(loadDialog());
assertThat(dialogFieldNames)
.as("flat image/alt fields should not coexist with the mediaItems multifield")
.doesNotContain("imagePath", "altText");
}
}
The second test is the one that fails against the state this scenario started in: no sling:resourceSuperType on the component, but hidden fields present anyway.
Dialog fix — delete, don't re-hide:
<!-- BEFORE: leftover fields marked hidden during the multifield migration -->
<imagePath
jcr:primaryType="nt:unstructured"
sling:hideResource="{Boolean}true"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Image Path"
name="./imagePath"
rootPath="/content/dam"/>
<altText
jcr:primaryType="nt:unstructured"
sling:hideResource="{Boolean}true"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Alt Text"
name="./altText"/>
<!-- AFTER: removed entirely - the mediaItems multifield is the only path -->
Model scanner used by the contract test — a small reflection-based helper, not a full static-analysis tool, deliberately kept simple:
final class ModelPropertyScanner {
private ModelPropertyScanner() {
}
static Set<String> scan(Class<?> modelClass) {
Set<String> properties = new HashSet<>();
for (Field field : modelClass.getDeclaredFields()) {
ValueMapValue valueMapValue = field.getAnnotation(ValueMapValue.class);
if (valueMapValue != null) {
String name = valueMapValue.name();
properties.add(name.isEmpty() ? field.getName() : stripLeadingDot(name));
}
ChildResource childResource = field.getAnnotation(ChildResource.class);
if (childResource != null) {
properties.add(field.getName());
}
}
return properties;
}
private static String stripLeadingDot(String name) {
return name.startsWith("./") ? name.substring(2) : name;
}
}
Content cleanup — a JCR query confirming production impact before deleting the field from the dialog, since deleting the dialog field doesn't remove any values already stored under old content:
SELECT [jcr:path] FROM [nt:unstructured]
WHERE ISDESCENDANTNODE('/content')
AND [imagePath] IS NOT NULL
Any hits from that query needed a one-time content report sent to the site's content team — not an automated delete, since a human should confirm none of those paths were actually still relevant before the property is orphaned for good.
imagePath value from before the migration — none in active use per the content team's review, all safe to leave orphaned rather than migrate.sling:resourceSuperType-less component with hidden fields — one had a second, unrelated dead field from an even older redesign.The contract test catches the actual failure mode — a dialog field with no reader — rather than the specific field names involved, so it protects every future dialog change on this component, not just this one incident. Deleting instead of re-hiding removes the temptation for the next person doing a "quick simplification" to accidentally resurface a dead field the way this one was resurfaced. And treating "hidden field, no resourceSuperType" as a hard signal gives a fast, mechanical way to tell a legitimate merge-exclusion apart from an abandoned leftover without having to read the component's whole history first.
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.