Skip to content

Conversation

@philvarner
Copy link
Contributor

@philvarner philvarner commented Dec 23, 2025

What I'm changing

A existing problem with the typing for various methods is that an implementer could not create their own implementation of OrderStatus and have additional properties in it be serialized in server responses. This is due to fastapi's use of type annotations for determining what serializer to use when returning an object from an endpoint. As defined, any object returned as the status (either with an Order or from the Order statuses endpoint) will be serialized as an Order Status, so any additional provider-specific attributes are not included. However, by adding the correct generic typing, a provider can redefine these endpoints so their own custom OrderStatus class will be serialized correctly.

How I did it

  1. Refactored the Callable aliases GetOrders, GetOrder, GetOrderStatuses as classes that implement dunder call, so that they can have generic parameters
  2. Refactor the uses of RootRouter in ProductRouter to have two protocols (ConformancesSupport and RootProvider), to decouple the actual (generified) RootRouter from the uses in that class.

Checklist

  • Tests pass: ./scripts/run-tests.sh
  • Checks pass: uv run pre-commit run --all-files
  • CHANGELOG is updated (if necessary)

@philvarner philvarner changed the title add typing to support variable OrderStatus objects Add better typing to support variable OrderStatus objects Dec 31, 2025
@philvarner philvarner changed the title Add better typing to support variable OrderStatus objects Generify typing to support OrderStatus subclass serialization Jan 5, 2026
@philvarner philvarner marked this pull request as ready for review January 5, 2026 19:43
@philvarner philvarner requested a review from gadomski as a code owner January 5, 2026 19:43
@gadomski gadomski removed their request for review January 5, 2026 23:55
@jkeifer
Copy link
Member

jkeifer commented Jan 16, 2026

@philvarner I'm sorry I have not had a chance to get to this review, I've been dealing with several rapidly-approaching deadlines. I just wanted to post that I have this on my radar and am trying to get to it, but practically I'm not sure I can until next week sometime.

Copy link
Member

@jkeifer jkeifer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generally looks good to me, though I have a rather large concern about switching the callable types to protocols (conceptually I agree the protocols are certainly better, but in practice they have limitations that I think are problematic).

I'm marking this with "request changes", but nothing I flagged is necessarily a required change if you can convince me the concerns I pointed out are unfounded. I'd be happy to chat about this on a call or something if that would be easier than async conversation.

from stapi_pydantic import OpportunitySearchRecord, OpportunitySearchStatus, Order, OrderStatusBound


class GetOrders(Protocol, Generic[OrderStatusBound]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why protocols here as opposed to individual callables? One key reason for the callables previously was to allow for using decorators in end-user implementations. This is especially relevant with the use of returns, because its decorators can greatly simplify result handling. Decorators are notoriously hard to get working with class methods, and unless something has changed in the returns implementation since I last looked, its decorators do not support the descriptor pattern necessary to accommodate class methods.

As a result, I'd strongly suggest reverting back to callables for these signatures.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to refresh the historical context, we moved away from protocols in this PR: stapi-spec/stapi-fastapi#132. At that time the major motivation was to break up what endpoints were supported by the routers in response to what "behaviors" were passed in via these callables, but the secondary effect of using callables was to allow for more flexible implementations as I noted above.

return self.url_for(request, f"{self.name}:{LIST_ORDER_STATUSES}", order_id=order_id)

def order_links(self, order: Order[OrderStatus], request: Request) -> list[Link]:
def order_links(self, order: Order[Any], request: Request) -> list[Link]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just need an education here, but what is the difference between Order[Any] and just Order? I get nervous whenever I see Any because it can disable type checking unexpectedly. I'm not sure if it would have that effect here being used as a generic type specifier, but it does make me wonder what effect it has.

Comment on lines +101 to +102
self.root_provider = root_provider
self.conformances_support: ConformancesSupport = root_provider
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the thinking behind having two properties for the same object? If a RootProvider necessarily is a ConformancesSupport, then why not just use the self.root_provider?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note: I do like this idea to use the RootProvider protocol here to decouple the ProductRouter from the RootRouter implementation. 👍

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.

3 participants