A blank string stored in a portableText field makes the entry unsaveable from the admin
What happens
An entry whose rich text field stores "" cannot be saved. Every save is refused with:
body: Invalid input: expected array, received stringAn image or file field in the same state saves fine, because normalizeMediaFields rewrites the value before validation runs. Nothing does that for portableText, so the blank string reaches validateContentData and fails there.
This traps an editor, which is why I am reporting it rather than leaving it. The admin reads the entry, gets "" back for the field, and sends it again on save. The error names a field the editor never touched and cannot see anything wrong with, and no edit made in the admin changes the value that gets sent, so there is no way out from the interface. Clearing it needs an API call sending an explicit null, which is accepted.
Blank strings get in through imports. Ours came from a Joomla migration where an empty column became "" rather than a missing key.
Reproduction
Against main (79c69b2), as an integration test in packages/core/tests/integration/content/:
beforeEach(async () => {
ctx = await setupForDialect(dialect);
const registry = new SchemaRegistry(ctx.db);
await registry.createCollection({ slug: "posts", label: "Posts" });
await registry.createField("posts", { slug: "title", label: "Title", type: "string" });
await registry.createField("posts", { slug: "body", label: "Body", type: "portableText" });
await registry.createField("posts", { slug: "hero", label: "Hero", type: "image" });
runtime = createTestRuntime(ctx.db);
const created = await runtime.handleContentCreate("posts", {
slug: "p1",
data: { title: "p1" },
status: "published",
});
if (!created.success) throw new Error("setup failed");
id = created.data.item.id;
});
it("an image field accepts a blank string", async () => {
const r = await runtime.handleContentUpdate("posts", id, { data: { title: "a", hero: "" } });
expect(r.success).toBe(true); // passes
});
it("a portableText field does not", async () => {
const r = await runtime.handleContentUpdate("posts", id, { data: { title: "b", body: "" } });
expect(r.success).toBe(false); // passes: "body: Invalid input: expected array, received string"
});Both assertions pass on main, which is to say the asymmetry is real: the same empty value is tolerated in one field type and rejected in the other.
Suggested fix
Treat a blank or whitespace-only string as "no value" for portableText on the way in, the same way #3129 does for media, so it is stored as null rather than refused. That also heals entries already carrying one, since the next save writes the canonical empty value.
This is the same underlying bug as #3129 in a different field type, so it may be worth fixing in the same place. I am happy to open a PR if you would like one; I have not assumed the shape you would prefer.
Scope
Worth checking whether any other field type validates a stored value that no normalizer canonicalises first. I only checked media and rich text.
Environment
- EmDash 0.38.0, reproduced against
mainat 79c69b2 - Cloudflare Workers and D1, and the sqlite test dialect above
- Found on a site of about 9,700 directory listings and 2,000 articles migrated from Joomla; one entry was affected
Source: emdash-cms/emdash