Skip to content

feat: implement the URL search params serialization standard - #611

Merged
razor-x merged 1 commit into
betafrom
claude/url-search-params-serializer
Aug 13, 2026
Merged

feat: implement the URL search params serialization standard#611
razor-x merged 1 commit into
betafrom
claude/url-search-params-serializer

Conversation

@razor-x

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

Copy link
Copy Markdown
Member

A Python port of @seamapi/url-search-params-serializer, exported for callers using their own HTTP client. Nothing in the SDK calls it yet — serializing the params of a request is a separate change.

The layer

The serialization defines the name and value of each search param, where every value is a string, and leaves rendering the query string to URLSearchParams. UrlSearchParams is that layer here, so serialize_url_search_params is a thin wrapper over it, exactly as the reference implementation wraps new URLSearchParams().

search_params = UrlSearchParams()
update_url_search_params(search_params, {"device_ids": ["device1", "device2"]})

list(search_params)   # => [('device_ids', 'device1'), ('device_ids', 'device2')]
str(search_params)    # => 'device_ids=device1&device_ids=device2'

Three places Python differs from JavaScript

These are where a naive port would produce the wrong bytes:

  • Encoding. The application/x-www-form-urlencoded serializer leaves * bare and escapes ~. urllib's quote_plus does the opposite, so the encoder is written against the byte set.
  • Sorting. URLSearchParams.sort() compares UTF-16 code units, so 😀 sorts before . Python's str comparison sorts by code point and gets that backwards; the sort key is name.encode("utf-16-be").
  • Numbers. repr(1.0) is "1.0" but JavaScript gives "1"; repr(1e-7) is "1e-07" vs "1e-7"; the exponent-notation thresholds differ too (Python switches at 1e16, JavaScript at 1e21). This implements the ECMAScript Number::toString algorithm on top of Python's shortest-round-trip repr.

Omitted params and null params

Python has one absence value and the standard needs both. Since sending null is rarely intended and unsetting a value cannot be undone, None keeps the safe meaning:

serialize_url_search_params({"name": NULL, "limit": 20})  # => 'limit=20&name='
serialize_url_search_params({"name": None, "limit": 20})  # => 'limit=20'

NULL is the value to pass; Null is its type, exported so a caller can annotate a wrapper. is_null is internal — the package does not export it.

Verification

Checked against the reference implementation running under Node:

  • 116 hand-built cases covering every rule in its README, including error messages
  • 4,000 randomized nested-param cases with unicode keys, mixed tuples, deep nesting, dates and bigints
  • 60,000 float cases including every power of ten from 1e-330 to 1e308, denormals, and random binary64 bit patterns

All match byte-for-byte. Separately, 22 cases round-trip through the parser the API uses in strict mode, which is documented as a true inverse of the serializer.

The 54 unit tests in this PR are the readable subset of that: the ported test suite plus the Python-specific cases (naive datetimes, Decimal-free float formatting, dict subclasses, sets rejected as non-deterministic).

Checks

Rebased onto beta after #612, so this runs against the updated tooling: black 26, pylint 4, pytest 9 and mypy 2. 141 tests pass (87 already on beta, 54 added here), pylint 10.00/10, black, rstcheck, and mypy seam test clean across 114 files.

This PR is purely additive — six files, no changes to pyproject.toml, uv.lock, seam/client.py, codegen, or the generated routes.

@razor-x
razor-x requested a review from a team as a code owner August 13, 2026 09:35
Port @seamapi/url-search-params-serializer to Python. It defines how the Seam
SDKs and other API consumers serialize objects to URL search params, and the
Seam API parses them with the corresponding parser.

Output is byte-for-byte identical to the reference implementation:

- Values are encoded with the application/x-www-form-urlencoded serializer,
  which differs from urllib in its treatment of "*" and "~".
- Params are sorted by name, compared by UTF-16 code unit.
- Floats are formatted using the ECMAScript Number::toString algorithm, which
  differs from repr for integral floats and around the exponent notation
  thresholds.

The serialization defines the name and value of each param, where every value
is a string, and leaves rendering the query string to URLSearchParams.
UrlSearchParams is that layer here.

Python has a single absence value, and the standard needs both: a param set to
None is omitted, while a param set to NULL is serialized to an empty value,
which the API reads as null.

Nothing calls this yet. Serializing the params of a request is a separate
change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PpqwDhZmyikGbjmCPqFW2A
@razor-x
razor-x force-pushed the claude/url-search-params-serializer branch from e88e338 to d214664 Compare August 13, 2026 16:12
@razor-x
razor-x merged commit 1219392 into beta Aug 13, 2026
20 checks passed
@razor-x
razor-x deleted the claude/url-search-params-serializer branch August 13, 2026 17:18
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