Skip to content

Make graph attributes iteration order canonical - #286

Merged
mbastian merged 1 commit into
masterfrom
graph-attributes-canonical-order
Aug 18, 2026
Merged

Make graph attributes iteration order canonical#286
mbastian merged 1 commit into
masterfrom
graph-attributes-canonical-order

Conversation

@mbastian

Copy link
Copy Markdown
Member

GraphAttributesImpl backed its map with a HashMap, and Serialization.serializeGraphAttributes iterates that map's entrySet() straight into the byte stream. HashMap iteration order is an implementation detail, so the serialized bytes were a function of insertion history rather than of content alone.

Switching to a TreeMap makes iteration sorted by key, so the same attributes always produce the same bytes regardless of the order they were set in.

TreeMap rejects null keys where HashMap accepted them, and throws NPE from deep inside the map on get(null) where HashMap returned null. Added Objects.requireNonNull(key, "key") to all nine public methods taking a key, so the failure is intentional and well-messaged rather than incidental.

deepHashCode and deepEquals are unchanged and remain correct — Map.hashCode() is specified as the order-independent sum of entry hash codes, and deepEquals goes through MapDeepEquals, which compares by key lookup.

The read path is unaffected, so previously written files still load identically. Only the ordering of newly written attribute entries changes; the format itself is untouched and Serialization.VERSION is not bumped.

1597 tests pass.

🤖 Generated with Claude Code

GraphAttributesImpl backed its map with a java.util.HashMap, and
Serialization.serializeGraphAttributes iterates that map's entrySet()
straight into the byte stream. HashMap iteration order is an
implementation detail, so the serialized bytes were a function of
insertion history rather than of content alone.

Switch the field to a TreeMap so iteration is sorted by key and the
output bytes become a pure function of the content. This is a
prerequisite for byte-pinned serialization fixtures.

TreeMap rejects null keys where HashMap accepted them, and throws NPE
from deep inside the map on get(null)/containsKey(null) where HashMap
returned null/false. Add explicit Objects.requireNonNull(key, "key")
guards to every public method taking a key so the failure is
intentional and well-messaged.

deepHashCode and deepEquals are unchanged and remain correct:
Map.hashCode() is specified as the order-independent sum of entry hash
codes, and deepEquals goes through MapDeepEquals which compares by key
lookup.

The read path is unaffected -- deserializeGraphAttributes just puts
entries into the map -- so previously written files still load
identically. Only the order of newly written graph-attribute entries
changes; the format itself is untouched and Serialization.VERSION is
not bumped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mbastian mbastian added this to the 0.8.7 milestone Aug 18, 2026
@mbastian
mbastian merged commit 626ef53 into master Aug 18, 2026
1 check passed
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