Skip to content

Conversation

@alukach
Copy link
Collaborator

@alukach alukach commented Nov 28, 2025

Related Issue(s):

Description:

Currently, the fields extension fails on the Collections endpoint due to the fact that we process the include and exclude arguments as set() values and then attempt to convert it to JSON. set values are not supported by json.dumps and thus an error is thrown.

clean_args = self._clean_search_args(
base_args=base_args,
datetime=datetime,
fields=fields,
sortby=sortby,
filter_query=filter_expr,
filter_lang=filter_lang,
**kwargs,
)
# NOTE: `FreeTextExtension` - pgstac will only accept `str` so we need to
# join the list[str] with ` OR `
# ref: https://github.com/stac-utils/stac-fastapi-pgstac/pull/263
if q := clean_args.pop("q", None):
clean_args["q"] = " OR ".join(q) if isinstance(q, list) else q
async with request.app.state.get_connection(request, "r") as conn:
q, p = render(
"""
SELECT * FROM collection_search(:req::text::jsonb);
""",
req=json.dumps(clean_args),
)

stack trace
___________ test_app_collection_fields_extension[api_client0-0.8.6] ____________

load_test_data = <function load_test_data.<locals>.load_file at 0x7f1de94337e0>
app_client = <httpx.AsyncClient object at 0x7f1de94d25d0>
load_test_collection = {'description': 'Landat 8 imagery radiometrically calibrated and orthorectified using gound points and Digital Elevati...[['2013-06-01T00:00:00Z', None]]}}, 'id': 'test-collection', 'keywords': ['landsat', 'earth observation', 'usgs'], ...}

    async def test_app_collection_fields_extension(
        load_test_data, app_client, load_test_collection
    ):
        fields = ["id", "title"]
>       resp = await app_client.get("/collections", params={"fields": ",".join(fields)})
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/api/test_api.py:278: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.13/site-packages/httpx/_client.py:1768: in get
    return await self.request(
.venv/lib/python3.13/site-packages/httpx/_client.py:1540: in request
    return await self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/httpx/_client.py:1629: in send
    response = await self._send_handling_auth(
.venv/lib/python3.13/site-packages/httpx/_client.py:1657: in _send_handling_auth
    response = await self._send_handling_redirects(
.venv/lib/python3.13/site-packages/httpx/_client.py:1694: in _send_handling_redirects
    response = await self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/httpx/_client.py:1730: in _send_single_request
    response = await transport.handle_async_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/httpx/_transports/asgi.py:170: in handle_async_request
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/fastapi/applications.py:1134: in __call__
    await super().__call__(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/applications.py:113: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:186: in __call__
    raise exc
.venv/lib/python3.13/site-packages/starlette/middleware/errors.py:164: in __call__
    await self.app(scope, receive, _send)
.venv/lib/python3.13/site-packages/stac_fastapi/api/middleware.py:83: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/middleware/cors.py:85: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/brotli_asgi/__init__.py:81: in __call__
    await br_responder(scope, receive, send)
.venv/lib/python3.13/site-packages/brotli_asgi/__init__.py:126: in __call__
    await self.app(scope, receive, self.send_with_brotli)
.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py:63: in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
    raise exc
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
    await app(scope, receive, sender)
.venv/lib/python3.13/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/routing.py:716: in __call__
    await self.middleware_stack(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/routing.py:736: in app
    await route.handle(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/routing.py:290: in handle
    await self.app(scope, receive, send)
.venv/lib/python3.13/site-packages/fastapi/routing.py:124: in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:53: in wrapped_app
    raise exc
.venv/lib/python3.13/site-packages/starlette/_exception_handler.py:42: in wrapped_app
    await app(scope, receive, sender)
.venv/lib/python3.13/site-packages/fastapi/routing.py:110: in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/fastapi/routing.py:390: in app
    raw_response = await run_endpoint_function(
.venv/lib/python3.13/site-packages/fastapi/routing.py:289: in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.venv/lib/python3.13/site-packages/stac_fastapi/api/routes.py:63: in _endpoint
    return _wrap_response(await func(request=request, **request_data.kwargs()))
                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
stac_fastapi/pgstac/core.py:102: in all_collections
    req=json.dumps(clean_args),
        ^^^^^^^^^^^^^^^^^^^^^^
../../_temp/uv-python-dir/cpython-3.13.9-linux-x86_64-gnu/lib/python3.13/json/__init__.py:231: in dumps
    return _default_encoder.encode(obj)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../_temp/uv-python-dir/cpython-3.13.9-linux-x86_64-gnu/lib/python3.13/json/encoder.py:200: in encode
    chunks = self.iterencode(o, _one_shot=True)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../../_temp/uv-python-dir/cpython-3.13.9-linux-x86_64-gnu/lib/python3.13/json/encoder.py:261: in iterencode
    return _iterencode(o, 0)
           ^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <json.encoder.JSONEncoder object at 0x7f1defcdb4d0>, o = {'id', 'title'}

    def default(self, o):
        """Implement this method in a subclass such that it returns
        a serializable object for ``o``, or calls the base implementation
        (to raise a ``TypeError``).
    
        For example, to support arbitrary iterators, you could
        implement default like this::
    
            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                # Let the base class default method raise the TypeError
                return super().default(o)
    
        """
>       raise TypeError(f'Object of type {o.__class__.__name__} '
                        f'is not JSON serializable')
E       TypeError: Object of type set is not JSON serializable

This PR converts these values to a list before json.dumps().

Additionally, I have added a test to validate this behavior. #327 and #328 came out of these tests, requiring the testing to be adjusted slightly.

PR Checklist:

  • pre-commit hooks pass locally
  • Tests pass (run make test)
  • Documentation has been updated to reflect changes, if applicable, and docs build successfully (run make docs)
  • Changes are added to the CHANGELOG.

@alukach alukach force-pushed the bug/collection-fields-extension branch 2 times, most recently from e4c9d3c to 4aa6542 Compare November 28, 2025 16:50
@alukach alukach force-pushed the bug/collection-fields-extension branch from f0fbab4 to a915f2a Compare November 28, 2025 19:02
@alukach alukach force-pushed the bug/collection-fields-extension branch 3 times, most recently from 4b57864 to b5e71b4 Compare November 28, 2025 20:21
@alukach alukach force-pushed the bug/collection-fields-extension branch from b5e71b4 to 04600a3 Compare November 28, 2025 20:45
@alukach alukach marked this pull request as ready for review November 28, 2025 20:51
@alukach alukach force-pushed the bug/collection-fields-extension branch from d1df3ca to 4452922 Compare November 29, 2025 01:56
Copy link
Collaborator

@hrodmn hrodmn left a comment

Choose a reason for hiding this comment

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

Thanks for fixing this @alukach!

@vincentsarago

This comment was marked as off-topic.

@alukach
Copy link
Collaborator Author

alukach commented Dec 1, 2025

Maybe I'm mistaken but there are no fields conformance class for collections (get_collections or get_collection) endpoint

Well I checked the PR and there is stac-api-extensions/fields#16 😓

but since we were supporting it, lets make it work 😅

I see you marked this comment as off-topic, but I will share that I was going off of: https://github.com/stac-api-extensions/collection-search/blob/v1.0.0-rc.1/README.md?plain=1#L103-L108

@alukach alukach enabled auto-merge (squash) December 2, 2025 00:14
@alukach alukach merged commit 25af093 into main Dec 2, 2025
8 checks passed
@alukach alukach deleted the bug/collection-fields-extension branch December 2, 2025 15:51
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.

fields extension not compatible when model_validation is enabled for /collections endpoints

4 participants