Skip to content

fix(#4328): pre-serialize properties so count matches bytes written in writeExternal#4349

Merged
robfrank merged 5 commits into
mainfrom
fix/4328-javabinaryserializer-property-count-mismatch
May 27, 2026
Merged

fix(#4328): pre-serialize properties so count matches bytes written in writeExternal#4349
robfrank merged 5 commits into
mainfrom
fix/4328-javabinaryserializer-property-count-mismatch

Conversation

@robfrank

Copy link
Copy Markdown
Collaborator

Closes #4328

Summary

JavaBinarySerializer.writeExternal wrote properties.size() as the property count but silently skipped writing size/payload bytes for null-valued properties and properties whose type could not be inferred (type == -1). Because readExternal always reads name + int-size + bytes for each counted entry, a single skipped property desynced every subsequent read, producing garbage values or EOFException.

The fix collects serializable entries (non-null value, recognised type) into parallel names/payloads lists in one pass - which also pre-serializes the bytes once each - then writes names.size() as the count before emitting the lists. The written count now matches the bytes on the wire exactly.

Test plan

  • JavaBinarySerializerTest#documentWithNullPropertyDoesNotDesync - document with a null-valued property in the middle of non-null properties round-trips correctly; null property absent, non-null properties intact.
  • JavaBinarySerializerTest#documentAllNullPropertiesDeserializesEmpty - document with only null properties serializes to an empty map.
  • Both new tests confirmed to FAIL before the fix (EOFException) and PASS after.
  • All 7 JavaBinarySerializerTest methods pass; 73 serializer/document tests pass with no regressions.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses a deserialization desynchronization issue (#4328) in JavaBinarySerializer by pre-filtering and pre-serializing properties to ensure the written property count matches the actual serialized entries. It also adds regression tests to verify the fix. The reviewer suggests a performance optimization to avoid multiple byte array allocations during pre-serialization by using a single ByteArrayOutputStream to reduce garbage collection pressure.

Comment on lines 57 to 88
final Map<String, Object> properties = document.toMap();
out.writeInt(properties.size());
final List<String> names = new ArrayList<>(properties.size());
final List<byte[]> payloads = new ArrayList<>(properties.size());
for (final Map.Entry<String, Object> prop : properties.entrySet()) {
final String propName = prop.getKey();
final Object propValue = prop.getValue();

// PROPERTY NAME
out.writeUTF(propName);

if (propValue != null) {
// PROPERTY VALUE
buffer.clear();

final Property property = documentType.getPropertyIfExists(propName);
final byte type = BinaryTypes.getTypeFromValue(propValue, property);
if (type == -1) {
// INVALID: SKIP IT
LogManager.instance()
.log(BinaryTypes.class, Level.WARNING,
"Cannot serialize property '%s' of type %s, value %s. The property will be ignored",
propName, propValue.getClass(), propValue);
continue;
}

buffer.putByte(type);
serializer.serializeValue(db, buffer, type, propValue);
buffer.flip();

out.writeInt(buffer.size());
out.write(buffer.getContent(), 0, buffer.size());
if (propValue == null)
continue;
final Property schemaProp = documentType.getPropertyIfExists(prop.getKey());
final byte type = BinaryTypes.getTypeFromValue(propValue, schemaProp);
if (type == -1) {
LogManager.instance()
.log(BinaryTypes.class, Level.WARNING,
"Cannot serialize property '%s' of type %s, value %s. The property will be ignored",
prop.getKey(), propValue.getClass(), propValue);
continue;
}
buffer.clear();
buffer.putByte(type);
serializer.serializeValue(db, buffer, type, propValue);
buffer.flip();
names.add(prop.getKey());
payloads.add(Arrays.copyOf(buffer.getContent(), buffer.size()));
}

// PROPERTY COUNT - matches exactly what will be written below
out.writeInt(names.size());
for (int i = 0; i < names.size(); i++) {
out.writeUTF(names.get(i));
final byte[] payload = payloads.get(i);
out.writeInt(payload.length);
out.write(payload);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performance Optimization: Avoid Multiple Byte Array Allocations

The current implementation pre-serializes properties by allocating a new byte[] array for every single valid property via Arrays.copyOf(buffer.getContent(), buffer.size()), and maintains two parallel ArrayLists (names and payloads). In high-throughput database operations, this creates significant garbage collection pressure.

We can optimize this by serializing all valid properties into a single temporary ByteArrayOutputStream wrapped in a DataOutputStream. This reduces the number of allocations to O(1) per document serialization (one ByteArrayOutputStream and one final byte[] via baos.toByteArray()), while maintaining full compatibility with readExternal.

    final Map<String, Object> properties = document.toMap();
    int validCount = 0;
    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (final DataOutputStream tempOut = new DataOutputStream(baos)) {
      for (final Map.Entry<String, Object> prop : properties.entrySet()) {
        final Object propValue = prop.getValue();
        if (propValue == null)
          continue;
        final String propName = prop.getKey();
        final Property schemaProp = documentType.getPropertyIfExists(propName);
        final byte type = BinaryTypes.getTypeFromValue(propValue, schemaProp);
        if (type == -1) {
          LogManager.instance()
              .log(BinaryTypes.class, Level.WARNING,
                  "Cannot serialize property '%s' of type %s, value %s. The property will be ignored",
                  propName, propValue.getClass(), propValue);
          continue;
        }
        buffer.clear();
        buffer.putByte(type);
        serializer.serializeValue(db, buffer, type, propValue);
        buffer.flip();

        tempOut.writeUTF(propName);
        tempOut.writeInt(buffer.size());
        tempOut.write(buffer.getContent(), 0, buffer.size());
        validCount++;
      }
      tempOut.flush();
    }

    // PROPERTY COUNT - matches exactly what will be written below
    out.writeInt(validCount);
    out.write(baos.toByteArray());

@codacy-production

codacy-production Bot commented May 26, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Coverage 100.00% diff coverage · -13.42% coverage variation

Metric Results
Coverage variation -13.42% coverage variation
Diff coverage 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (61177a8) 127719 94366 73.89%
Head commit (cd51460) 159403 (+31684) 96378 (+2012) 60.46% (-13.42%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#4349) 17 17 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@claude

claude Bot commented May 26, 2026

Copy link
Copy Markdown

Code Review

Overall: This is a correct and well-explained bug fix. The root cause analysis is accurate and the regression tests are solid. A few items worth discussing before merge.


What the PR does

writeExternal was writing properties.size() as the property count, then silently skipping null-valued properties and properties with unrecognized types (type == -1) without writing their size/payload bytes. Because readExternal always reads readUTF() + readInt() + read(array) for each counted entry, a single skipped property desynced every subsequent read, producing garbage values or EOFException.

The fix pre-serializes all valid properties into a staging ByteArrayOutputStream, counts only those that were actually serialized, then writes validCount followed by the staged bytes. The written count now matches the wire bytes exactly.


Issues

1. docs/ file should be removed

docs/4328-javabinaryserializer-property-count-mismatch.md is a per-issue design doc that duplicates the PR description. Per project convention, per-issue analysis docs are not committed - the fix lives in the code and the context lives in the PR/commit message. This file will become stale and confusing once the PR is merged. Please drop it.

2. Pre-existing in.read(array) may not read all bytes (not introduced here, but worth flagging)

In readExternal at line 131:

in.read(array);

ObjectInput.read(byte[]) is not guaranteed to fill the buffer in one call (it returns the number of bytes actually read, just like InputStream.read). For large property payloads this can silently truncate values. Should be:

in.readFully(array);

This is a pre-existing bug and out of scope for this PR, but since you are touching this class it would be a good opportunity to fix it.

3. Minor: extra heap buffer allocation

The staging ByteArrayOutputStream allocates and grows a heap buffer holding a copy of all serialized properties. A two-pass approach avoids this at the cost of iterating the in-memory map twice:

// Pass 1: count valid entries
int validCount = 0;
for (final Map.Entry<String, Object> prop : properties.entrySet()) {
  if (prop.getValue() == null) continue;
  final byte type = BinaryTypes.getTypeFromValue(prop.getValue(), documentType.getPropertyIfExists(prop.getKey()));
  if (type != -1) validCount++;
}
out.writeInt(validCount);

// Pass 2: serialize
for (final Map.Entry<String, Object> prop : properties.entrySet()) { ... }

For documents with many large properties this halves peak heap pressure. Since serialization is not on the query hot path this is a minor concern, but it is simpler code too - no DataOutputStream wrapper, no staged.toByteArray() copy.


What is good

  • Root cause is correct. The count/bytes mismatch is exactly the bug and the fix closes the gap cleanly.
  • Regression tests are well-designed. documentWithNullPropertyDoesNotDesync specifically tests the desync scenario (null property in the middle of non-null ones), and documentAllNullPropertiesDeserializesEmpty covers the degenerate all-null case. Both use AssertJ style matching project conventions.
  • The try-with-resources on DataOutputStream is correct even though ByteArrayOutputStream.close() is a no-op; good habit.
  • Backward compatibility is not a concern. Any previously serialized document with null properties was already undeserializable due to the bug, so there is no valid old format to support.
  • The Javadoc on line 39 was updated to match the new behavior (Null properties are excluded from serialization).

Summary

Fix the in.read(array)in.readFully(array) issue while you are here (even as a separate commit), drop the docs/ file, and either merge as-is or consider the two-pass approach if you want to avoid the staging buffer. The core logic is correct and the tests are solid.

@robfrank

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. Addressed point 1 (dropped docs/ file) in 2cf6346.

On point 2 (in.read -> in.readFully): Already being fixed in #4348 (currently open). The bot's own note flags it as out of scope, and pulling it in here would create a conflict with that PR. Leaving for #4348.

On point 3 (two-pass instead of staging buffer): The staging-buffer shape came from gemini's review in the first round (5a3e679 -> 3053f8b). A two-pass approach would re-invoke BinaryTypes.getTypeFromValue and serializer.serializeValue per property to avoid the staging copy. Since serialization is not on the query hot path - as the review notes - I prefer one allocation over duplicate compute. Keeping the staging buffer.

@claude

claude Bot commented May 26, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR fixes a real data-corruption bug in JavaBinarySerializer.writeExternal: the old code wrote properties.size() as the count but silently skipped null/unrecognized-type entries, causing readExternal to desync on every subsequent property. The fix pre-serializes valid entries into a staging buffer, then writes the true count before the buffered bytes. The root cause analysis and fix are both correct.


Code Quality

writeExternal logic is sound. Collecting entries into a ByteArrayOutputStream before writing the count is the simplest correct approach given that ObjectOutput does not extend OutputStream, so staged.writeTo(out) is not directly callable.

One avoidable extra copy - staged.toByteArray() allocates a new byte array on every call. Since writeExternal is always called from MutableDocument.writeExternal with an ObjectOutputStream, you could cast and use staged.writeTo((OutputStream) out) to skip the extra allocation:

// instead of: out.write(staged.toByteArray())
staged.writeTo((OutputStream) out);

Minor, but worth it for large documents.

Comment style - CLAUDE.md says comments should explain the why rather than the what, capped at one line. The two-line staging comment and // PROPERTY COUNT - matches exactly what will be written below both describe what the code does. Either drop them or condense to a single non-obvious constraint note.


Pre-existing issue surfaced by the fix (not introduced here)

In readExternal (line 131):

in.read(array);   // may return fewer bytes than array.length

ObjectInput.read(byte[]) follows the InputStream.read contract and is not guaranteed to fill the buffer in one call. For payloads spanning multiple network segments this can silently truncate a property. The correct replacement is:

in.readFully(array);

ObjectInputStream already implements DataInput, so readFully is available. Since this sits right next to the fixed code, fixing it in the same pass would be clean.


Tests

Two regression tests are included, confirmed to fail before the fix and pass after - good practice.

Assertion style - line 164 uses:

assertThat(result.containsKey("nullProp")).isFalse();

The more idiomatic AssertJ form (better failure message, matches project style) is:

assertThat(result).doesNotContainKey("nullProp");

Summary

The fix is correct and the tests cover the important desync scenario. Suggested follow-ups:

  1. Replace in.read(array) with in.readFully(array) in readExternal - correctness gap, pre-existing.
  2. Optionally: staged.writeTo((OutputStream) out) to avoid the extra byte-array copy.
  3. assertThat(result.containsKey(...)).isFalse() -> assertThat(result).doesNotContainKey(...).
  4. Trim redundant comments per CLAUDE.md conventions.

@robfrank

Copy link
Copy Markdown
Collaborator Author

Applied in 5188a4e:

  • assertThat(result).doesNotContainKey("nullProp") (idiomatic AssertJ)
  • Trimmed the what-style comments per CLAUDE.md

Declined:

  • staged.writeTo((OutputStream) out) - the method signature is ObjectOutput, not ObjectOutputStream. The cast holds today only because MutableDocument happens to call us with ObjectOutputStream. If a future caller passes any other ObjectOutput implementation, the cast would throw ClassCastException. The single toByteArray() allocation is the price of respecting the interface.
  • in.read(array) -> in.readFully(array) - same suggestion as the previous cycle; being addressed in fix(#4329): use readFully instead of read in JavaBinarySerializer.readExternal #4348.

@claude

claude Bot commented May 26, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR fixes a desync bug in JavaBinarySerializer.writeExternal (closes #4328). The root cause: the original code wrote properties.size() as the property count up-front, then silently skipped null-valued and unrecognized-type properties during iteration - but readExternal unconditionally consumed exactly that many entries, causing the read cursor to fall out of sync for every subsequent property.

The fix stages all valid entries into a ByteArrayOutputStream in one pass, then writes validCount followed by the staged bytes - so the written count always matches what is on the wire.

The Fix is Correct

The two-pass approach cleanly solves the desync: null and unrecognized-type properties are filtered before the count is committed to the stream. The logic is easy to follow and the fix is minimal.

Findings

One pre-existing bug worth fixing alongside this PR (readExternal)

In readExternal at line 129:

in.read(array);

ObjectInput extends DataInput, which provides readFully. The plain read(byte[]) call is not guaranteed to fill the buffer in one shot - it may return a short read on slow streams or large payloads. This should be:

in.readFully(array);

Not introduced by this PR, but since the test suite is already exercising this path it is a good moment to close the gap.

Minor: unnecessary heap copy

staged.toByteArray() (line 87) allocates a full copy of the staged buffer. The allocation is harmless for correctness and for most use cases, but it is extra GC pressure on documents with many/large properties. One option is to write validCount into staged first (or use a separate 4-byte prefix), then call staged.writeTo(wrappedOutputStream) - but since ObjectOutput is not an OutputStream the current approach is the pragmatic choice. Worth a comment or a ticket if this path is hot.

One naming nit

The local variable staged is clear; the parallel variable tempOut vs out is a bit confusing since both are output streams in scope. stagedOut would be unambiguous.

Test Coverage

Both new tests are well-structured:

  • documentWithNullPropertyDoesNotDesync covers the exact reported scenario (null in the middle of non-null properties).
  • documentAllNullPropertiesDeserializesEmpty covers the degenerate all-null edge case.
  • The assertion style (assertThat(...).containsEntry(...).doesNotContainKey(...)) matches the project convention.
  • The PR description confirms both tests fail before the fix and pass after - good discipline.

One small suggestion: the second test calls doc2.toMap(false) to exclude @cat/@type metadata, but the comment explaining why is only in-line. A companion assertion verifying that the non-metadata map is indeed empty (e.g. hasSize(0)) would make intent even clearer, though isEmpty() already does this.

Summary

The fix is correct and well-tested. The only actionable suggestion is to change in.read(array) to in.readFully(array) in readExternal while this code is under review - it is a latent bug on the read side that mirrors the write-side bug this PR fixes.

@robfrank

Copy link
Copy Markdown
Collaborator Author

Cycle 4 (final per --max-cycles=4):

Stopping the orchestrator here. Merge belongs to the maintainer.

@claude

claude Bot commented May 26, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR fixes a real data-corruption bug in JavaBinarySerializer.writeExternal. The old code wrote properties.size() as the property count (which included null-valued and unserializable properties), but then skipped writing size+bytes for those entries. Since readExternal unconditionally reads name + int-size + bytes for each counted entry, any null or unserializable property caused complete stream desync.

The fix is correct and the root cause analysis in the PR description is accurate.


What works well

  • Correct approach: Staging to a ByteArrayOutputStream first, then writing validCount before the payload, is clean and eliminates the count/bytes mismatch.
  • Backward compatibility: Documents with no null properties and all valid types produce an identical wire format - the staged bytes contain the same name+size+data sequence as before. Only the (already-broken) null-property case changes.
  • Test coverage: The two new tests directly reproduce the reported failure (EOFException on desync) and verify the fix. The TestHelper base class drops and recreates the database after each test, so both tests creating a type named "Doc" is safe.
  • Logging preserved: The WARNING log for unserializable types is kept, which aids debugging.

Issues and suggestions

Pre-existing bug worth noting (out of scope but related): readExternal at line 129 uses in.read(array) which is not guaranteed to fill the buffer - ObjectInputStream.read(byte[]) may return fewer bytes than requested if the stream is chunked. This should be in.readFully(array) for correctness. This is pre-existing and not introduced by this PR, but it sits right next to the fix and could cause a similar class of silent data corruption on large property values.

Minor - extra allocation: staged.toByteArray() creates a copy of the already-buffered bytes. For Java serialization (not the hot binary path) this is acceptable. An alternative would be a two-pass approach (count valid entries first, then write), but that would either serialize values twice or require storing intermediate results - the staged-buffer approach is simpler and more readable.

Minor - validCount is redundant with staged: After the try-with-resources block, validCount equals the number of entries written to stagedOut. It's clear and correct as-is, but it's worth noting it could also be derived by tracking entries in a list. Not a problem.

Comment style: The CLAUDE.md guidance asks to avoid comments that explain what code does rather than why. The added comment // Count must be written before the payload, so stage the entries to know the final count up-front. documents a real non-obvious constraint (ordering of count vs. bytes on the wire), which satisfies the "WHY is non-obvious" bar.


Summary

The fix is correct, well-tested, and safe to merge. The one thing worth addressing before or shortly after merging is in.read(array)in.readFully(array) in readExternal to close a related reliability gap on the read side.

@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 58.26%. Comparing base (61177a8) to head (cd51460).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #4349      +/-   ##
============================================
- Coverage     64.72%   58.26%   -6.46%     
+ Complexity      446      423      -23     
============================================
  Files          1647     1647              
  Lines        127719   127725       +6     
  Branches      27375    27375              
============================================
- Hits          82660    74422    -8238     
- Misses        33384    42643    +9259     
+ Partials      11675    10660    -1015     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

robfrank added 5 commits May 27, 2026 08:38
…n writeExternal

writeExternal wrote properties.size() as the count but silently skipped the
size/payload bytes for null-valued and unrecognised-type properties. readExternal
always reads UTF + int + bytes for each counted entry, so a single skipped
property desynced every subsequent read (EOFException or garbage values).

Fix: collect serializable (non-null, known-type) entries into parallel name/payload
lists in one pass, then write names.size() as the count before iterating the lists.
Replace the two parallel ArrayLists + per-property Arrays.copyOf with a
single growing ByteArrayOutputStream wrapped in a DataOutputStream. This
collapses O(N) per-property byte[] allocations down to one final
toByteArray copy, easing GC pressure for documents with many properties.

Wire format is unchanged - readExternal sees the same byte sequence.
claude bot review: per-issue analysis docs are not committed in this project;
the PR description and commit messages carry the context. The file would
become stale once merged.
- Test uses doesNotContainKey for idiomatic AssertJ
- Drop comments that describe what the code does; keep only the why
Two 'Out' variables (tempOut wrapping staged, and the parameter 'out') in scope
were easy to confuse. stagedOut makes the relationship to 'staged' explicit.
@robfrank robfrank force-pushed the fix/4328-javabinaryserializer-property-count-mismatch branch from 317e730 to cd51460 Compare May 27, 2026 06:38
@claude

claude Bot commented May 27, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR fixes a correctness bug in JavaBinarySerializer.writeExternal: the old code wrote properties.size() as the property count but silently skipped null-valued and unrecognized-type properties when emitting their payloads. Because readExternal always reads count entries, one skipped property desynced all subsequent reads, producing garbage values or EOFException.

The fix stages serializable entries into a ByteArrayOutputStream, counts them, then writes the count followed by the staged bytes. The fix is correct and the approach is sound.


Correctness - No Issues

The root cause is accurately diagnosed and the fix is logically correct. The written count now exactly matches the number of entries on the wire. Both null-skip and type=-1-skip paths are handled consistently.


Potential Issues

Pre-existing bug in readExternal (not introduced here, but worth fixing)

// JavaBinarySerializer.java:129
final byte[] array = new byte[propertySize];
in.read(array);  // <-- may read fewer bytes than requested

ObjectInput.read(byte[]) is not guaranteed to fill the array - it behaves like InputStream.read(). This should be in.readFully(array). Under normal ObjectInputStream use this rarely triggers, but it is a latent correctness hole worth fixing in a follow-up.

Double logging for unrecognized types

BinaryTypes.getTypeFromValue already logs a WARNING when it returns -1, and then writeExternal logs a second WARNING for the same event. This was present in the old code and is preserved here, but results in two log lines per unrecognized property. Minor, but worth noting.


Performance

The fix allocates a ByteArrayOutputStream and DataOutputStream wrapper on every writeExternal call, adding GC pressure. The project has a strong performance/GC-minimization mandate.

A lighter alternative: the thread-local db.getContext().getTemporaryBuffer2() (8 KB, pre-allocated) could stage the raw bytes, but encoding Java's writeUTF format manually into Binary is fragile. The current approach trades a small per-call allocation for clear, maintainable code - an acceptable trade-off given the fix is for correctness. If writeExternal ever becomes a hot path, a reusable staging buffer per thread-context would be the right next step.

Also, staged.toByteArray() at line 87 creates a defensive copy of the internal buffer. A micro-optimization: staged.writeTo((OutputStream) out) avoids that copy (valid since ObjectOutputStream extends OutputStream), though the cast is slightly fragile. Not a blocking concern.


Test Coverage

The two new tests are well-structured and use the correct AssertJ assertion style:

  • documentWithNullPropertyDoesNotDesync - covers the primary bug scenario (null in the middle of valid properties)
  • documentAllNullPropertiesDeserializesEmpty - covers the all-null edge case

Suggestion: Add a third test for the unrecognized-type (type == -1) path, analogous to the null test. The unrecognized-type branch is the other skip path and currently has no explicit regression coverage.


Minor Notes

  • The // Count must be written before... comment is appropriate - it explains a non-obvious staging constraint.
  • The removed inline // PROPERTY COUNT, // PROPERTY NAME, // PROPERTY VALUE comments are not missed; the code is self-explanatory without them.
  • Wire format compatibility: data serialized with the old buggy code was already unreadable (desync), so the format change is not a real regression.

Summary

The fix is correct and the tests confirm the before/after behavior. The main actionable items are:

  1. (This PR or follow-up) Fix in.read(array) to in.readFully(array) in readExternal
  2. (Optional) Add a test for the unrecognized-type (type == -1) skip path
  3. (Optional) Consider staged.writeTo((OutputStream) out) to avoid the defensive copy in toByteArray()

@robfrank robfrank merged commit f2a41f8 into main May 27, 2026
25 of 30 checks passed
@robfrank robfrank deleted the fix/4328-javabinaryserializer-property-count-mismatch branch May 27, 2026 07:48
@robfrank robfrank added this to the 26.6.1 milestone May 27, 2026
@robfrank robfrank self-assigned this May 27, 2026
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.

JavaBinarySerializer.writeExternal writes a property count that doesn't match the bytes written

1 participant