Skip to content

fix(api-model): DataWrapper.items is a List, so a caller can read it by position - #141

Merged
olavgg merged 1 commit into
mainfrom
fix/datawrapper-getitems-list
Sep 29, 2026
Merged

olavgg merged 1 commit into
mainfrom
fix/datawrapper-getitems-list

Conversation

@JosteinGj

Copy link
Copy Markdown
Contributor

Why

DataWrapper.items is an ordered JSON array on the wire, in the order the caller asked for —
sortBy for a read, insertion order for a bulk write. It was typed as Collection<T>, which
throws that ordering away at the type level, so there is no way to address a result by position.

Reading the first of one result therefore had to be written:

client.timeseries().retrieve(request).getItems().iterator().next()

This turned up as a concrete cost rather than a style opinion. Fixing the SDK documentation
(IntelliStream-DataHub/datahub-sdk-docs#103) found 27 examples across 26 pages that all said
getItems().get(0), and none of them had ever compiled:

cannot find symbol: method get(int) (in interface Collection<DatapointsCollection>)

The Python client expresses the same read as retrieve_datapoints(rf)[0].get_datapoints(). The
envelope's type was the only reason the Java tab of every one of those pages had to be uglier
than the client it documents — and iterator().next() is also worse on failure, throwing a
bare NoSuchElementException with no message when the result is empty.

What changes

The getter narrows to List<T>. The setter still accepts any Collection, so the graph
endpoints — whose nodes and relations are sets — keep compiling untouched.

Three behaviours are held fixed deliberately, each pinned by a test in
DataWrapperItemsListTest:

Behaviour Why it must not change
A list is held as given, not copied A caller mutating the list it passed still sees the envelope change, as before
A non-list is copied, keeping its iteration order The graph endpoints pass Sets
null stays null Several request validators test getItems() == null to reject a body that sent "items": null; an absent array must not arrive as an empty one

The wire shape is unchanged in both directions — serialization and deserialization round-trip
identically, including nextCursor.

Why not Iterable<T>

Considered first, since it is two lines, and measured: Jackson does not collapse the envelope
into a bare array (bean properties win over add-on Iterable detection), so that risk is not
real. It was rejected for two other reasons:

  1. It does not help. The case in the docs is the first of one, so you would still write
    resp.iterator().next() — character for character what is already there.
  2. It adds a footgun. DataWrapper carries nextCursor, so for (Event e : page) reads like
    "every event" and silently processes one page.

If for-each over results is wanted later, the honest form is a paging helper on the service that
follows nextCursor, not an Iterable envelope that hides the existence of paging.

Verified

  • compileJava + compileTestJava across every module: all 279 getItems()/setItems() call
    sites compile unchanged.
  • :datahub-api-model:test, :datahub-java-sdk:test, :datahub-infra:test, :datahub-api:test
    all green.
  • DataWrapperItemsListTest fails to compile against main, so it is a real guard rather
    than a restatement.
  • Built the patched jars and compiled docs/guides/detect-events.mdx's original Java verbatim
    against them: it fails on main with the error above and compiles here.

Docs follow-up

datahub-sdk-docs#103 worked around this by rewriting those 27 sites to .iterator().next() —
that is what makes the docs' compile job green today, and it should merge on its own schedule.
Once this lands, those sites can go back to .get(0), which is both what they said originally
and what the Python and Rust tabs beside them look like. Happy to raise that revert as a separate
PR; it needs no coordination with this one, since .iterator().next() keeps compiling either way.

🤖 Generated with Claude Code

…by position

`items` is an ordered JSON array on the wire, in the order the caller asked for —
`sortBy` for a read, insertion order for a bulk write — but it was typed as
`Collection<T>`, which throws that away. Every caller reading a single result had
to write

    client.timeseries().retrieve(request).getItems().iterator().next()

because `Collection` has no `get(int)`. This is not hypothetical: the SDK docs
described `getItems().get(0)` on 27 pages and none of them compiled, and the
Python client says `retrieve_datapoints(rf)[0]` for the same read. The envelope
was the only reason Java looked worse than the language it wraps.

The getter narrows to `List<T>`; the setter still takes any `Collection`, so the
graph endpoints that hand over a `Set` keep compiling. Three behaviours are held
fixed on purpose, and `DataWrapperItemsListTest` pins each:

- a list is held as given, not copied, so a caller mutating the list it passed
  still sees the envelope change;
- a non-list is copied and keeps that collection's iteration order;
- `null` is still stored as `null` — several request validators test
  `getItems() == null` to reject a body that sent `"items": null`, and an absent
  array must not arrive as an empty one.

The wire shape does not change in either direction. `Iterable<T>` was considered
instead and rejected: it does not help read the first of one, and an envelope
carrying `nextCursor` that answers a for-each with a single page invites a silent
paging bug.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: jgjesdal <jostein@intellistream.ai>
@JosteinGj
JosteinGj requested a review from olavgg as a code owner September 28, 2026 13:04
@olavgg
olavgg merged commit 68b8924 into main Sep 29, 2026
9 checks passed
@olavgg
olavgg deleted the fix/datawrapper-getitems-list branch September 29, 2026 12:24
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.

2 participants