feat!: multi-decoder routing (decoders=[...])#41
Merged
Conversation
Reframes the decoder seam from one-decoder-per-client to a typed-dispatched `decoders=[...]` list. Removes the 0.3.0 eager-import fail-fast: `AsyncClient()` no longer raises on missing pydantic. Adds `MissingDecoderError` (fires before the HTTP call). Target release: 0.9.0.
Nine-task TDD sequence covering the AsyncClient/Client migration to `decoders=[...]`, the new MissingDecoderError, and the docs sweep. Phases A (new surfaces), B (atomic client refactors), C (new test files), D (docs).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the single-decoder slot on
AsyncClient/Clientwith a type-dispatcheddecoders=[...]list. Reverses the 0.3.0 fail-fast for missing pydantic —AsyncClient()no longer raises on missing extras; failure is deferred to the firstresponse_model=use site via the newMissingDecoderError(fires before the HTTP call).Headline changes
ResponseDecoderProtocol gainscan_decode(model) -> bool. The client walks_decodersin order and picks the first whose predicate returns True. Built-in decoders claim broadly within their library; native types of the other library are rejected (pydantic rejectsmsgspec.Struct; msgspec rejectspydantic.BaseModelviamsgspec.inspect.type_info+CustomTypefilter).decoders: Sequence[ResponseDecoder] | None = Nonereplaces the olddecoder=kwarg on bothAsyncClient.__init__andClient.__init__. DefaultNoneresolves via_build_default_decoders()against installed extras (pydantic-first when both present, empty tuple when neither).MissingDecoderError(sibling ofDecodeErrorunderClientError) carriesmodel: typeandregistered_names: tuple[str, ...]. Pre-flight check at.send()/.send_with_response()entry — distinct fromDecodeError(decoder ran, payload bad).AsyncClient(decoders=[PydanticDecoder(), MsgspecDecoder()])is the new default;BaseModelroutes to pydantic,Structto msgspec, shared shapes (dict, list, dataclass, primitive) go to the first decoder in the list._default_pydantic_decoder()helper deleted.Behavioral changes for users
AsyncClient(decoder=...)now raisesTypeError: unexpected keyword argument 'decoder'. Migration:AsyncClient(decoders=[...]).AsyncClient()/Client()no longer raiseImportErrorwhen pydantic is missing. Errors now surface only whenresponse_model=is used.ResponseDecoderimplementations must addcan_decode. Trivial catch-all migration:def can_decode(self, model): return True.Spec + plan
planning/specs/2026-06-09-multi-decoder-design.mdplanning/plans/2026-06-09-multi-decoder-plan.mdplanning/engineering.md.Target release
0.9.0— minor bump, breaking surface (!in commit subjects).Test plan
just lintclean (ruff format + ruff check + ty)just testgreen — 485 passed, 100% coveragemkdocs build --strictclean (no warnings on new content)AsyncClientandClientread identically moduloawait)MissingDecoderErrorfires before_dispatch(request)(asserted viapytest.failin MockTransport handlers in 4+ tests)decoder=kwarg anywhere insrc//tests//docs/pip install httpware[pydantic,msgspec]and verify mixed-decoder client works end-to-endpip uninstall pydantic, verifyAsyncClient()constructs without raising