Skip to content

Skip the encoder for null optional elements that write nothing - #83

Merged
veewee merged 8 commits into
absent-optional-elementsfrom
perf-skip-absent-non-nillable-elements
Oct 16, 2026
Merged

veewee merged 8 commits into
absent-optional-elementsfrom
perf-skip-absent-non-nillable-elements

Conversation

@veewee

@veewee veewee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member
Q A
Type improvement
BC Break no
Fixed issues

Stacked on #79, so it targets that branch until it is merged.

Null optional elements on encode

A null value on an optional element that is not nillable goes through ErrorHandlingEncoder and OptionalElementEncoder, only to come back as an empty string that writes nothing. On sparse payloads that is most of the elements, and it is the largest cost left in sparse encoding.

ObjectAccess::build now asks the encoder of every child element once per type what a null value does, and keeps the answer in one onNull map. The answer is a NullValueHandling case: Write lets the encoder write it, Skip means the encoder writes nothing for null, and SkipWhenAbsent leaves the element out when the property is absent and writes xsi:nil when it is set to null. Encoders answer through a new Feature\NullValueAware interface. OptionalElementEncoder answers Skip for a nullable element that is not nillable, the same rule its iso uses, SkipWhenAbsent for an optional nillable element, and Write otherwise. ErrorHandlingEncoder forwards the question. Attributes and the _ text value always get Write, and a custom encoder lens never counts as absent, as in #79. ObjectEncoder::to has one null check that reads this map, and replaces the absent check from #79 as well.

Same output

This does not depend on how the encoders are nested. Any encoder that does not implement the interface keeps its own null handling, so a custom encoder that writes something for null is never skipped, and a custom encoder can opt in. Custom encoder lenses still run before the check. One thing does change compared to #79: a custom OptionalAware encoder that is not wrapped in OptionalElementEncoder no longer has its absent optional nillable elements left out unless it implements NullValueAware, because only the encoder knows what it writes for null.

One output detail changes: an object whose elements are all null encoded to <a></a> and now encodes to <a/>. Both are the same empty element, and an object whose nillable optional elements are all absent already gets <a/> since #79. The tests pin the output byte for byte for all null, all absent, leading and trailing nulls, a nested empty object and a custom encoder. The encoded requests and decoded results of the 3MB workloads have the same sha1 before and after, because none of their objects is empty.

Speed

Base is absent-optional-elements (#79). Medians over three interleaved rounds on the same machine, with ext-soap as the noise reference.

Workload #79 This PR
3MB encode sparse, optional elements without nillable (stdClass) 1.46 s 0.99 s
3MB encode sparse, optional elements without nillable (classmap) 1.60 s 1.13 s
3MB encode dense (stdClass) 0.72 s 0.71 s
3MB encode sparse, absent nillable elements (stdClass) 1.05 s 1.06 s
3MB decode sparse (stdClass) 0.50 s 0.49 s
EncodeSparseBench sparse x500 15.5 ms 15.5 ms
EncodeSparseBench all null x500 25.6 ms 26.6 ms
EncodeBench literal x500 33.9 ms 33.6 ms
ext-soap encode literal x500 3.79 ms 3.66 ms

The first two rows are the target and drop by about a third. EncodeSparseBench uses nillable elements, which this change does not skip, so those rows and the others stay within the run to run spread.

A null value on an optional element that is not nillable goes through
ErrorHandlingEncoder and OptionalElementEncoder only to come back as an
empty string. ObjectAccess now records per property whether that is the
case, and ObjectEncoder skips the iso for those nulls.

The empty write still closes the start tag of the parent, so the first
skipped element of an object keeps it to write <a></a> as before. Custom
encoders that handle null themselves are not wrapped in the optional
element encoder and keep their output.
Every expectation also holds on the base branch, so the skip leaves the output of these cases as it was.
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 09a53411-2349-423c-99ab-31e572d85acd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

ObjectAccess no longer depends on ErrorHandlingEncoder wrapping OptionalElementEncoder directly. An encoder that implements WritesNothingForNull answers for itself, ErrorHandlingEncoder forwards the question, and any other encoder keeps its own null handling.
An object whose elements are all null now encodes as <a/> instead of <a></a>. Both are the same empty element; the short form is what an object without children already gets elsewhere.
Two booleans per property, one for absent nillable elements and one for elements that write nothing, described a single decision from two sides. The encoder now answers it once through NullValueAware with a NullValueHandling case, and ObjectAccess keeps one map of those answers.

A custom OptionalAware encoder that is not wrapped in OptionalElementEncoder no longer has its absent optional nillable elements left out, unless it implements NullValueAware itself, because only the encoder can tell what it writes for null.
@veewee
veewee merged commit 7b37bfa into absent-optional-elements Oct 16, 2026
24 checks passed
@veewee
veewee deleted the perf-skip-absent-non-nillable-elements branch October 16, 2026 08:49
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