Skip to content

feat: apply the URL search params standard and type nullable params - #614

Merged
razor-x merged 4 commits into
betafrom
claude/hook-up-url-search-params-serializer
Aug 13, 2026
Merged

feat: apply the URL search params standard and type nullable params#614
razor-x merged 4 commits into
betafrom
claude/hook-up-url-search-params-serializer

Conversation

@razor-x

@razor-x razor-x commented Aug 13, 2026

Copy link
Copy Markdown
Member

Hooks the serializer added in #611 into the client, so requests actually go
out with the standard applied, and consumes the blueprint isNullable flag so
NULL is typed precisely. No method changes: routes keep using
preferredMethod exactly as they do today.

Serializing request params

SeamHttpClient.request now does two things before handing off to httpx:

  • Any mapping passed as params is run through
    serialize_url_search_params, and the resulting query string is set on the
    url with httpx.URL(url, query=query.encode()). httpx never sees the
    params, so it never re-encodes them.
  • NULL sentinels in a json body are replaced with None, via a new
    replace_null in seam/null.py, so json.dumps emits them as null.
    Without it a body carrying the sentinel raises
    TypeError: Object of type Null is not JSON serializable.

Params that are not a mapping — an already-built query string, or the pairs
of a UrlSearchParams — are passed through to httpx untouched, so a caller
who serialized them already keeps control of the representation.

Why the query is set on the url

httpx would otherwise re-encode what the serializer produced. The two
disagree on arrays and nested objects, and on exactly two characters:

params httpx this PR
{"device_ids": []} ? device_ids=
{"custom_metadata_has": {"k": "v"}} custom_metadata_has=%7B%27k%27%3A+%27v%27%7D custom_metadata_has.k=v
{"search": "a *~ b"} search=a+%2A~+b search=a+*%7E+b

The character differences are cosmetic — the API decodes both to the same
pairs — but the array and nested-object rows are not: the empty array
currently disappears from the query entirely, and a nested object currently
goes out as the repr of a Python dict.

The query survives verbatim, including through relative-url merging against
base_url, because httpx carries it in raw_path.

Typing nullable params

nullableType renders a param the API documents as nullable as
Union[<type>, Null], so it accepts the sentinel:

seam.devices.update(device_id="...", name=NULL)       # name is nullable
seam.devices.update(device_id="...", is_managed=NULL) # fails the type check

A param that is merely optional deliberately does not accept it. Optional
params are omitted by passing None, where sending null would unset a value
instead, so accepting the sentinel everywhere would invite exactly the mistake
it exists to prevent.

This covers 49 params across 20 route modules — 43 Union[str, Null], 4
Union[float, Null], 2 Union[int, Null] — and the from ..null import Null
import is emitted only for modules that need it.

Scope

Under preferredMethod a route only gets GET/DELETE when all of its
params are simple, since getPreferredMethod falls back to POST for
complex ones. So generated routes exercise the simple path, and the array and
nested-object paths are reached through direct seam.client.get(...) calls.

Tests

test/search_params_test.py asserts on the exact bytes the SDK puts on the
wire via the recording server: standard serialization, no re-encoding, None
omitted, NULL sent empty, no ? when there are no params, every verb,
non-mapping pass-through, UnserializableParamError raised before the
request, NULL in a JSON body, and a generated route.

test/nullable_param_test.py covers the three states end to end — NULL sent
as null, None omitted, a real value sent — on both a nullable string and a
nullable number. Because mypy runs over test/ in CI, these also fail the
Lint job if a nullable param ever loses its Null type.

The recording server now records method, the raw target, and path/query
split apart, and answers every verb rather than just GET and POST
/devices/update is PATCH, which the old handler answered with a 501.

159 passing, up from 141 on beta. pylint 10.00/10, black, mypy,
rstcheck, tsc, eslint, and prettier clean; npm run generate is
idempotent.

The end-to-end round trip through the API's parser — e.g.
seam.client.get("/devices/list", params={"device_ids": []}) returning zero
devices — still needs the @seamapi/fake-seam-connect 2.x bump, since the
pinned 1.86.0 has no parser and rejects device_ids=. That bump belongs with
the semanticMethod change, where it is required.

README

Adds a Setting a Param to Null section covering the omit-versus-null
distinction, None versus NULL, why null is never the default, Null as
the exported type, and which params accept the sentinel.

The Seam API parses URL search params as complex types, so the SDK has to
build the query string itself. Serialize any mapping passed as params and
set the result on the url, rather than letting httpx encode the params with
its own rules, which represent arrays and nested objects differently.

Replace the NULL sentinel with null in request bodies as well, so a param
set to NULL is sent as null on either transport, and document how NULL
tells an explicitly null param apart from an omitted one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpqwDhZmyikGbjmCPqFW2A
@razor-x
razor-x requested a review from a team as a code owner August 13, 2026 17:50
Comment thread seam/client.py Outdated
Comment thread README.rst Outdated
razor-x and others added 2 commits August 13, 2026 11:12
Co-authored-by: Evan Sosenko <razorx@evansosenko.com>
Consume the blueprint isNullable flag so a param the Seam API documents as
nullable is typed to accept NULL, and a param that is merely optional is not.
Optional params are omitted by passing None, where sending null would unset a
value instead, so accepting the sentinel everywhere would invite exactly the
mistake it exists to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpqwDhZmyikGbjmCPqFW2A
@razor-x razor-x changed the title feat: serialize request params with the URL search params standard feat: apply the URL search params standard and type nullable params Aug 13, 2026
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpqwDhZmyikGbjmCPqFW2A
@razor-x
razor-x merged commit 52cfee7 into beta Aug 13, 2026
20 checks passed
@razor-x
razor-x deleted the claude/hook-up-url-search-params-serializer branch August 13, 2026 18:30
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