Skip to content

fix(rich-text): serialize a text link as an object - #105

Merged
brd6 merged 2 commits into
mainfrom
fix/link-serialization
Jul 23, 2026
Merged

brd6 merged 2 commits into
mainfrom
fix/link-serialization

Conversation

@brd6

@brd6 brd6 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Description

Link did not extend AbstractJsonSerializable, so its protected properties were invisible to json_encode. Every other property resource in the library extends that base class; Link was the exception.

The consequence is that a rich text carrying a link serialized as "link": [] instead of "link": {"url": "..."}.

$text = Text::fromContent('Linked');
$text->getText()?->setLink((new Link())->setUrl('https://example.test/a'));

$block = (new ParagraphBlock())->setParagraph(
    (new ParagraphProperty())->setRichText([$text]),
);

$block->toArrayForCreate();
// text.link => []   (before)
// text.link => ['url' => 'https://example.test/a']   (after)

This one-line change makes Link extend AbstractJsonSerializable, which is all that is needed for the existing toArray() machinery to serialize it. Reading is unaffected: Link::fromRawData() already worked, and a round-trip test covers it.

Motivation and context

The empty array is rejected by the Notion API, so creating a block with a linked rich text fails outright:

body failed validation: body.children[0].paragraph.rich_text[0].text.link
should be an object, `null`, or `undefined`, instead was `[]`.

Setting the rich text's href instead looks like a viable workaround, but it is not: the API accepts href on create, returns 200, and then silently discards it. The text arrives unlinked and merged into its adjacent runs, so callers get a success response and no link. Serializing text.link as an object is the only shape that produces a hyperlink.

How has this been tested?

  • Added tests/Resource/LinkTest.php covering the serialized shape, the omitted empty type, a linked rich text inside toArrayForCreate(), an unlinked rich text (no link key), and a fromRawData() round-trip.
  • Full suite: 241 tests, 1234 assertions, all passing on PHP 8.4.
  • phpcs and phpstan clean.
  • Verified against the live Notion API: appending a callout whose CTA carries text.link.url returns 200 and the created block comes back with the rich-text runs kept separate and href populated on the linked run. The same payload built with href alone comes back with href: null and the runs merged.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

PR checklist

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING.md document.
  • I have added tests to cover my changes.

brd6 added 2 commits July 23, 2026 13:03
Link did not extend AbstractJsonSerializable, so its protected properties
were invisible to json_encode and a linked rich text serialized as
"link": []. The API rejects that outright on create:

  body.children[0].paragraph.rich_text[0].text.link should be an object,
  `null`, or `undefined`, instead was `[]`

Callers could work around it by setting the rich text's href instead, but
the API silently ignores href on create — it returns 200, drops the link
and merges the run into its neighbours, so the text arrives unlinked.

Extending the base class makes the link serialize as { "url": ... },
which is the only shape that produces a real hyperlink.
The nullsafe operator is 8.0+, and the library supports ^7.4 || ^8.
@brd6
brd6 merged commit 860cda6 into main Jul 23, 2026
21 checks passed
@brd6
brd6 deleted the fix/link-serialization branch July 23, 2026 12:51
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