Skip to content

Reduce string conversions and allocations in RowJsonDeserializer and RowJsonSerializer - #40770

Open
sjvanrossum wants to merge 9 commits into
apache:masterfrom
sjvanrossum:row-json-opt
Open

sjvanrossum wants to merge 9 commits into
apache:masterfrom
sjvanrossum:row-json-opt

Conversation

@sjvanrossum

Copy link
Copy Markdown
Contributor

There were previously no microbenchmarks measuring Row to/from JSON serialization and deserialization throughput across flat and nested/composite schemas. Adds RowJsonBenchmark covering both String and byte[] pathways (RowJson and JsonUtils).

JsonUtils and JsonPayloadSerializerProvider previously decoded byte[] payloads into a new String(..., UTF_8) before parsing and serialized Rows to a String before calling .getBytes(UTF_8). Adds direct byte[] helpers on RowJsonUtils (jsonBytesToRow / rowToJsonBytes) using Jackson's ObjectReader.readValue(byte[]) and ObjectWriter.writeValueAsBytes(Object) to read and write UTF-8 bytes directly.

RowJsonDeserializer and RowJsonSerializer allocated an AutoValue FieldValue wrapper on every field, used Stream pipelines (IntStream, StreamSupport) to iterate over arrays/rows, and used SequentialRead sequential lists for array values. Replaces Stream pipelines and FieldValue wrappers with direct loops, ArrayList sizing, and Row.withSchema(schema).attachValues(...).

RowJsonDeserializer previously called jsonParser.readValueAsTree() to materialize an entire Jackson ObjectNode / ArrayNode AST in memory before extracting Row fields. Streams tokens directly from JsonParser in a single pass with ParserExtractors, caches the bound ObjectReader / ObjectWriter in JsonUtils, and avoids intermediate tree allocations altogether.

Benchmark Current (ops/us) Proposed (ops/us) Speedup
deserializeComplexRowBytes 0.298 ± 0.006 0.415 ± 0.015 +39.3% (1.39x)
deserializeComplexRowString 0.285 ± 0.021 0.413 ± 0.009 +44.9% (1.45x)
deserializeFlatRowString 0.471 ± 0.014 0.531 ± 0.024 +12.7% (1.13x)
serializeComplexRowBytes 2.085 ± 0.115 2.226 ± 0.119 +6.8% (1.07x)
serializeComplexRowString 2.068 ± 0.101 2.106 ± 0.068 +1.8% (1.02x)

Note

This PR stacks on top of #40769

Adds an EnumerationType field to RowJsonTest.ValueTests.makeLogicalTypeTestCase()
to verify serialization, deserialization, and round-tripping of logical types
whose input type (EnumerationType.Value) differs from their base type (INT32).
RowJson only special-cased SQL date/time logical types by identifier and
fell through to the underlying base FieldType for all other logical types
without converting between the logical input type (T) and base type (BaseT),
causing ClassCastExceptions on serialize and deserialize.
@github-actions github-actions Bot added java core size/l L: 250-999 lines changed labels Oct 24, 2026

@github-actions github-actions 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.

📋 Review Summary

This PR optimizes JSON-to-Row and Row-to-JSON conversions by replacing JsonNode tree materialization in RowJsonDeserializer with streaming JsonParser token extraction, avoiding intermediate String/byte[] conversions in JsonUtils and JsonPayloadSerializerProvider, and making ObjectMapper lazy/transient in JsonUtils. It also fixes two existing bugs: custom error field schemas in JsonToRowWithErrFn and non-SQL LogicalType base/input conversions in RowJson.

🔍 General Feedback

  • The streaming JsonParser refactoring in RowJson and RowJsonValueExtractors is clean, preserves existing bounds-checking and error-reporting semantics, and eliminates significant object allocation overhead.
  • One minor subtlety to watch out for is chained logical types (a LogicalType whose base type is another LogicalType), which is handled in Row.getBaseValue / Row.Equals via a while loop, whereas RowJsonSerializer currently only unwraps a single level.

💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.

gen.writeString(((LocalDateTime) value).toString()); // ISO 8601 format
} else {
writeValue(gen, type.getLogicalType().getBaseType(), value);
writeValue(gen, logicalType.getBaseType(), logicalType.toBaseType(value));

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.

🟢 Low: While RowJsonDeserializer recursively unwraps chained logical types via extractJsonValue(fieldName, logicalType.getBaseType(), jsonParser) (unless the inner type is one of SqlTypes.DATE/TIME/DATETIME, which expects the input type rather than the base type), RowJsonSerializer calls writeValue(gen, logicalType.getBaseType(), logicalType.toBaseType(value)). If a custom LogicalType wraps SqlTypes.DATE, SqlTypes.TIME, or SqlTypes.DATETIME as its base type, deserializer will extract a LocalDate/LocalTime/LocalDateTime and pass it to the outer logicalType.toInputType(...), which works as expected—just something to keep in mind.

Comment thread sdks/java/core/src/test/java/org/apache/beam/sdk/transforms/JsonToRowTest.java Outdated
@sjvanrossum
sjvanrossum force-pushed the row-json-opt branch 2 times, most recently from 64fe061 to b6d9946 Compare October 24, 2026 22:21
Verifies that JsonToRow.withExtendedErrorInfo() propagates custom lineField
and errorField names to both the dead-letter PCollection schema and the
emitted error Rows.
@github-actions

Copy link
Copy Markdown
Contributor

Checks are failing. Will not request review until checks are succeeding. If you'd like to override that behavior, comment assign set of reviewers

`JsonToRowWithErrFn.expand()` hardcoded the default 'line' and 'err' field
names when registering the dead-letter PCollection schema instead of using
`lineField()` and `errorField()`, causing a schema mismatch when custom field
names were configured.
@sjvanrossum

Copy link
Copy Markdown
Contributor Author

Run Java Examples_Dataflow PreCommit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core java size/l L: 250-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant