Skip to content

Add URL search params serialization for Seam API requests - #314

Draft
razor-x wants to merge 3 commits into
mainfrom
claude/csharp-serializer-pattern-bf15bp
Draft

Add URL search params serialization for Seam API requests#314
razor-x wants to merge 3 commits into
mainfrom
claude/csharp-serializer-pattern-bf15bp

Conversation

@razor-x

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

Copy link
Copy Markdown
Member

Summary

This PR adds comprehensive URL search parameter serialization support to the C# SDK, enabling proper serialization of complex types to query strings for HTTP GET requests to the Seam API. This includes a new Null sentinel type to distinguish between omitted parameters and explicitly null values.

Key Changes

  • UrlSearchParams: A new mutable collection class that manages URL search parameter pairs, supporting append, set, delete, sort, and encoding/decoding operations with WHATWG application/x-www-form-urlencoded serialization.

  • UrlSearchParamsSerializer: A port of the reference @seamapi/url-search-params-serializer implementation that serializes objects to URL search parameters with byte-for-byte compatibility. Features include:

    • Nested object support via dot-notation paths
    • Array serialization with multiple values per parameter name
    • Proper number formatting using ECMAScript Number::toString algorithm
    • DateTime/DateTimeOffset serialization to ISO 8601 format
    • Comprehensive type support (bool, string, integral types, float, double, decimal)
    • Parameter sorting by UTF-16 code units with stable sort for array element order
  • StrictUrlSearchParamsSerializer: A wrapper around the base serializer that appends _strict=true to non-empty queries for Seam API strict, schema-aware parsing.

  • Null sentinel: A new Null.Value type that explicitly represents null values in request parameters:

    • Serializes to JSON null in request bodies
    • Serializes to empty string in query parameters
    • Distinguishes from C# null which means "omit this parameter"
    • Includes custom JSON converter for automatic serialization
  • UnserializableParamError: A new exception type thrown when parameters cannot be serialized, with detailed error messages indicating the problematic parameter name and reason.

  • Comprehensive test coverage: Added 392 tests for serialization behavior, 131 tests for UrlSearchParams operations, 63 tests for strict serialization, and 53 tests for the Null sentinel.

  • Documentation: Updated README files with examples of setting null values and using the serializers with custom HTTP clients.

Notable Implementation Details

  • The serializer handles edge cases like NaN, Infinity, negative zero, and sub-millisecond precision truncation for DateTime values
  • Empty strings and empty arrays are handled specially: empty strings are omitted from parameters, while empty arrays serialize to name=
  • Nested objects with only empty children are completely omitted from the output
  • The implementation maintains byte-for-byte compatibility with the JavaScript reference implementation

https://claude.ai/code/session_01BLJcDjH3YHxDgtPgfq82g4

razor-x and others added 3 commits August 14, 2026 06:30
Port @seamapi/url-search-params-serializer to C# as
Seam.Client.UrlSearchParamsSerializer over a Seam.Client.UrlSearchParams
pair collection, byte-for-byte identical to the TypeScript reference
implementation. The test suite mirrors the reference and the other SDK
suites, covering every branch of the standard.

The .NET primitives each diverge from the standard, so the port
implements them directly: Uri.EscapeDataString is RFC 3986 flavored
(escapes *, keeps ~) where the WHATWG form encoding does the opposite,
and WebUtility.UrlEncode emits lowercase hex; the shortest
round-tripping number format switches to exponent notation at the wrong
thresholds and spells them E+16, so numbers follow the ECMAScript
Number::toString algorithm; and List.Sort is unstable, which would lose
array element order, so the sort is stable and ordinal, as
URLSearchParams.sort() is.

C# has a single absence value, so Seam.Client.Null adds the explicit
null sentinel: null means the safe option of omitting a param, and
sending null is always spelled Null.Value. The sentinel declares its own
JsonConverter, so a param set to it is sent as JSON null in a request
body under any serializer settings, while an omitted param is still
dropped by EmitDefaultValue. Every SDK request sends a JSON body, so
that is where the sentinel reaches the API; the serializers are exported
for callers that build their own requests.

StrictUrlSearchParamsSerializer wraps the base serializer and appends
_strict=true to any non-empty query, telling the Seam API to use strict,
schema-aware parsing. The flag is appended after the sort so it always
sits last, a caller-supplied _strict param is replaced rather than
repeated, and a query with no serializable params stays empty. The flag
is Seam API behavior, not part of the serialization standard, so it is
isolated in the wrapper and the base serializer stays a pure
implementation of the standard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLJcDjH3YHxDgtPgfq82g4
Consume the blueprint's preferredMethod so a route calls the client
method the Seam API prefers for it, rather than posting everything. Nine
endpoints become GET and one becomes DELETE; the rest keep a body as
POST, PUT or PATCH.

Which transport carries the params follows from the method, so the
generated route stays the same shape for every endpoint and the client
decides: a GET or DELETE serializes its params into the query string
with StrictUrlSearchParamsSerializer, and everything else sends a JSON
body as before. This is what applies the serialization standard to the
SDK's own requests, where until now it was only exported for callers
building their own.

The params are converted to search params through the request's JSON
contract rather than by reflection, so a param carries the same name and
the same value on either transport: DataMember names, string enum
values, and EmitDefaultValue omission all behave as they do in a body. A
param left unset is absent from that contract, so a null in it can only
be the Null sentinel and is restored as one, which serializes to an
empty value.

One byte of the query is not the serializer's: Uri normalizes a
percent-encoded unreserved character back to its literal form, so `~`
reaches the wire as `~` rather than as `%7E`. Both decode to the same
param.

The new transport tests assert what reaches an HttpListener for each
preferred method: the request line, the serialized query, and the body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLJcDjH3YHxDgtPgfq82g4
Three elements prove nothing about stability: below 17 elements .NET's
introsort degrades to an insertion sort, which keeps a short array in
order whether or not the sort preserves it. Sort 32 instead, and assert
the same string the PHP, Python, Ruby and JavaScript SDKs produce for
the same params.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BLJcDjH3YHxDgtPgfq82g4
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.

1 participant