From 625d2cb7b7c8cf3527833e87d068dbb78caddcae Mon Sep 17 00:00:00 2001
From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com>
Date: Mon, 18 Nov 2024 18:21:25 +0100
Subject: [PATCH] =?UTF-8?q?=20=20=F0=9F=8E=A8=20web-server=20api:=20orderi?=
 =?UTF-8?q?ng=20parameters=20and=20simplified=20openapi=20specs=20for=20co?=
 =?UTF-8?q?mplex=20query=20parameters=20(#6737)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 api/specs/web-server/_common.py               |  66 ++-
 api/specs/web-server/_folders.py              |  57 +--
 api/specs/web-server/_groups.py               |  28 +-
 api/specs/web-server/_projects_crud.py        |  86 ++--
 api/specs/web-server/_resource_usage.py       | 148 ++-----
 api/specs/web-server/_trash.py                |  12 +-
 .../src/models_library/rest_base.py           |  19 +
 .../src/models_library/rest_ordering.py       | 106 ++++-
 .../src/models_library/rest_pagination.py     |   3 +-
 .../models_library/utils/common_validators.py |  14 +
 .../tests/test_rest_ordering.py               | 139 +++++++
 .../pytest_simcore/helpers/assert_checks.py   |  10 +-
 .../servicelib/aiohttp/requests_validation.py |  13 +-
 .../src/servicelib/fastapi/openapi.py         |   2 +-
 .../tests/aiohttp/test_requests_validation.py |  44 +-
 .../tests/fastapi/test_request_decorators.py  |   3 +-
 .../simcore_service_agent/core/application.py |   2 +-
 .../core/application.py                       |   2 +-
 .../core/application.py                       |   2 +-
 .../api/v0/openapi.yaml                       | 376 +++++++-----------
 .../api_keys/_handlers.py                     |  17 +-
 .../clusters/_handlers.py                     |  28 +-
 .../director_v2/_handlers.py                  |   7 +-
 .../folders/_folders_handlers.py              |  14 +-
 .../folders/_models.py                        |  55 +--
 .../long_running_tasks.py                     |  13 +-
 .../src/simcore_service_webserver/models.py   |  11 +
 .../products/_handlers.py                     |  11 +-
 .../products/_invitations_handlers.py         |   5 +-
 .../projects/_common_models.py                |  12 +-
 .../projects/_crud_handlers.py                |  20 +-
 .../projects/_crud_handlers_models.py         |  76 ++--
 .../_pricing_plans_admin_handlers.py          |  51 ++-
 .../resource_usage/_pricing_plans_handlers.py |  24 +-
 .../resource_usage/_service_runs_handlers.py  | 131 +++---
 .../simcore_service_webserver/tags/schemas.py |   6 +-
 .../users/_preferences_handlers.py            |  13 +-
 .../wallets/_groups_handlers.py               |  20 +-
 .../wallets/_handlers.py                      |   8 +-
 .../workspaces/_groups_handlers.py            |  20 +-
 .../workspaces/_workspaces_handlers.py        |  57 ++-
 .../test_usage_services__list.py              |  26 +-
 .../with_dbs/04/workspaces/test_workspaces.py |  21 +
 43 files changed, 906 insertions(+), 872 deletions(-)
 create mode 100644 packages/models-library/src/models_library/rest_base.py
 create mode 100644 packages/models-library/tests/test_rest_ordering.py
 create mode 100644 services/web/server/src/simcore_service_webserver/models.py

diff --git a/api/specs/web-server/_common.py b/api/specs/web-server/_common.py
index f3dcd66bc5c..b6e1cf68769 100644
--- a/api/specs/web-server/_common.py
+++ b/api/specs/web-server/_common.py
@@ -8,15 +8,68 @@
 from typing import Any, ClassVar, NamedTuple
 
 import yaml
-from fastapi import FastAPI
+from fastapi import FastAPI, Query
 from models_library.basic_types import LogLevel
-from pydantic import BaseModel, Field
+from models_library.utils.json_serialization import json_dumps
+from pydantic import BaseModel, Field, create_model
 from pydantic.fields import FieldInfo
 from servicelib.fastapi.openapi import override_fastapi_openapi_method
 
 CURRENT_DIR = Path(sys.argv[0] if __name__ == "__main__" else __file__).resolve().parent
 
 
+def _create_json_type(**schema_extras):
+    class _Json(str):
+        __slots__ = ()
+
+        @classmethod
+        def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
+            # openapi.json schema is corrected here
+            field_schema.update(
+                type="string",
+                # format="json-string" NOTE: we need to get rid of openapi-core in web-server before using this!
+            )
+            if schema_extras:
+                field_schema.update(schema_extras)
+
+    return _Json
+
+
+def as_query(model_class: type[BaseModel]) -> type[BaseModel]:
+    fields = {}
+    for field_name, model_field in model_class.__fields__.items():
+
+        field_type = model_field.type_
+        default_value = model_field.default
+
+        kwargs = {
+            "alias": model_field.field_info.alias,
+            "title": model_field.field_info.title,
+            "description": model_field.field_info.description,
+            "gt": model_field.field_info.gt,
+            "ge": model_field.field_info.ge,
+            "lt": model_field.field_info.lt,
+            "le": model_field.field_info.le,
+            "min_length": model_field.field_info.min_length,
+            "max_length": model_field.field_info.max_length,
+            "regex": model_field.field_info.regex,
+            **model_field.field_info.extra,
+        }
+
+        if issubclass(field_type, BaseModel):
+            # Complex fields
+            field_type = _create_json_type(
+                description=kwargs["description"],
+                example=kwargs.get("example_json"),
+            )
+            default_value = json_dumps(default_value) if default_value else None
+
+        fields[field_name] = (field_type, Query(default=default_value, **kwargs))
+
+    new_model_name = f"{model_class.__name__}Query"
+    return create_model(new_model_name, **fields)
+
+
 class Log(BaseModel):
     level: LogLevel | None = Field("INFO", description="log level")
     message: str = Field(
@@ -120,6 +173,9 @@ def assert_handler_signature_against_model(
         for field in model_cls.__fields__.values()
     ]
 
-    assert {p.name for p in implemented_params}.issubset(  # nosec
-        {p.name for p in specs_params}
-    ), f"Entrypoint {handler} does not implement OAS"
+    implemented_names = {p.name for p in implemented_params}
+    specified_names = {p.name for p in specs_params}
+
+    if not implemented_names.issubset(specified_names):
+        msg = f"Entrypoint {handler} does not implement OAS: {implemented_names} not in {specified_names}"
+        raise AssertionError(msg)
diff --git a/api/specs/web-server/_folders.py b/api/specs/web-server/_folders.py
index ef5e29ac85d..c2e75579b26 100644
--- a/api/specs/web-server/_folders.py
+++ b/api/specs/web-server/_folders.py
@@ -9,19 +9,20 @@
 
 from typing import Annotated
 
-from fastapi import APIRouter, Depends, Query, status
+from _common import as_query
+from fastapi import APIRouter, Depends, status
 from models_library.api_schemas_webserver.folders_v2 import (
     CreateFolderBodyParams,
     FolderGet,
     PutFolderBodyParams,
 )
-from models_library.folders import FolderID
 from models_library.generics import Envelope
-from models_library.rest_pagination import PageQueryParameters
-from models_library.workspaces import WorkspaceID
-from pydantic import Json
 from simcore_service_webserver._meta import API_VTAG
-from simcore_service_webserver.folders._models import FolderFilters, FoldersPathParams
+from simcore_service_webserver.folders._models import (
+    FolderSearchQueryParams,
+    FoldersListQueryParams,
+    FoldersPathParams,
+)
 
 router = APIRouter(
     prefix=f"/{API_VTAG}",
@@ -36,7 +37,9 @@
     response_model=Envelope[FolderGet],
     status_code=status.HTTP_201_CREATED,
 )
-async def create_folder(_body: CreateFolderBodyParams):
+async def create_folder(
+    _body: CreateFolderBodyParams,
+):
     ...
 
 
@@ -45,20 +48,7 @@ async def create_folder(_body: CreateFolderBodyParams):
     response_model=Envelope[list[FolderGet]],
 )
 async def list_folders(
-    params: Annotated[PageQueryParameters, Depends()],
-    folder_id: FolderID | None = None,
-    workspace_id: WorkspaceID | None = None,
-    order_by: Annotated[
-        Json,
-        Query(
-            description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
-            example='{"field": "name", "direction": "desc"}',
-        ),
-    ] = '{"field": "modified_at", "direction": "desc"}',
-    filters: Annotated[
-        Json | None,
-        Query(description=FolderFilters.schema_json(indent=1)),
-    ] = None,
+    _query: Annotated[as_query(FoldersListQueryParams), Depends()],
 ):
     ...
 
@@ -68,19 +58,7 @@ async def list_folders(
     response_model=Envelope[list[FolderGet]],
 )
 async def list_folders_full_search(
-    params: Annotated[PageQueryParameters, Depends()],
-    text: str | None = None,
-    order_by: Annotated[
-        Json,
-        Query(
-            description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
-            example='{"field": "name", "direction": "desc"}',
-        ),
-    ] = '{"field": "modified_at", "direction": "desc"}',
-    filters: Annotated[
-        Json | None,
-        Query(description=FolderFilters.schema_json(indent=1)),
-    ] = None,
+    _query: Annotated[as_query(FolderSearchQueryParams), Depends()],
 ):
     ...
 
@@ -89,7 +67,9 @@ async def list_folders_full_search(
     "/folders/{folder_id}",
     response_model=Envelope[FolderGet],
 )
-async def get_folder(_path: Annotated[FoldersPathParams, Depends()]):
+async def get_folder(
+    _path: Annotated[FoldersPathParams, Depends()],
+):
     ...
 
 
@@ -98,7 +78,8 @@ async def get_folder(_path: Annotated[FoldersPathParams, Depends()]):
     response_model=Envelope[FolderGet],
 )
 async def replace_folder(
-    _path: Annotated[FoldersPathParams, Depends()], _body: PutFolderBodyParams
+    _path: Annotated[FoldersPathParams, Depends()],
+    _body: PutFolderBodyParams,
 ):
     ...
 
@@ -107,5 +88,7 @@ async def replace_folder(
     "/folders/{folder_id}",
     status_code=status.HTTP_204_NO_CONTENT,
 )
-async def delete_folder(_path: Annotated[FoldersPathParams, Depends()]):
+async def delete_folder(
+    _path: Annotated[FoldersPathParams, Depends()],
+):
     ...
diff --git a/api/specs/web-server/_groups.py b/api/specs/web-server/_groups.py
index 1f8d7e15f56..9fa015bd7b5 100644
--- a/api/specs/web-server/_groups.py
+++ b/api/specs/web-server/_groups.py
@@ -48,7 +48,7 @@ async def list_groups():
     response_model=Envelope[GroupGet],
     status_code=status.HTTP_201_CREATED,
 )
-async def create_group(_b: GroupCreate):
+async def create_group(_body: GroupCreate):
     """
     Creates an organization group
     """
@@ -58,7 +58,7 @@ async def create_group(_b: GroupCreate):
     "/groups/{gid}",
     response_model=Envelope[GroupGet],
 )
-async def get_group(_p: Annotated[_GroupPathParams, Depends()]):
+async def get_group(_path: Annotated[_GroupPathParams, Depends()]):
     """
     Get an organization group
     """
@@ -69,8 +69,8 @@ async def get_group(_p: Annotated[_GroupPathParams, Depends()]):
     response_model=Envelope[GroupGet],
 )
 async def update_group(
-    _p: Annotated[_GroupPathParams, Depends()],
-    _b: GroupUpdate,
+    _path: Annotated[_GroupPathParams, Depends()],
+    _body: GroupUpdate,
 ):
     """
     Updates organization groups
@@ -81,7 +81,7 @@ async def update_group(
     "/groups/{gid}",
     status_code=status.HTTP_204_NO_CONTENT,
 )
-async def delete_group(_p: Annotated[_GroupPathParams, Depends()]):
+async def delete_group(_path: Annotated[_GroupPathParams, Depends()]):
     """
     Deletes organization groups
     """
@@ -91,7 +91,7 @@ async def delete_group(_p: Annotated[_GroupPathParams, Depends()]):
     "/groups/{gid}/users",
     response_model=Envelope[list[GroupUserGet]],
 )
-async def get_all_group_users(_p: Annotated[_GroupPathParams, Depends()]):
+async def get_all_group_users(_path: Annotated[_GroupPathParams, Depends()]):
     """
     Gets users in organization groups
     """
@@ -102,8 +102,8 @@ async def get_all_group_users(_p: Annotated[_GroupPathParams, Depends()]):
     status_code=status.HTTP_204_NO_CONTENT,
 )
 async def add_group_user(
-    _p: Annotated[_GroupPathParams, Depends()],
-    _b: GroupUserAdd,
+    _path: Annotated[_GroupPathParams, Depends()],
+    _body: GroupUserAdd,
 ):
     """
     Adds a user to an organization group
@@ -115,7 +115,7 @@ async def add_group_user(
     response_model=Envelope[GroupUserGet],
 )
 async def get_group_user(
-    _p: Annotated[_GroupUserPathParams, Depends()],
+    _path: Annotated[_GroupUserPathParams, Depends()],
 ):
     """
     Gets specific user in an organization group
@@ -127,8 +127,8 @@ async def get_group_user(
     response_model=Envelope[GroupUserGet],
 )
 async def update_group_user(
-    _p: Annotated[_GroupUserPathParams, Depends()],
-    _b: GroupUserUpdate,
+    _path: Annotated[_GroupUserPathParams, Depends()],
+    _body: GroupUserUpdate,
 ):
     """
     Updates user (access-rights) to an organization group
@@ -140,7 +140,7 @@ async def update_group_user(
     status_code=status.HTTP_204_NO_CONTENT,
 )
 async def delete_group_user(
-    _p: Annotated[_GroupUserPathParams, Depends()],
+    _path: Annotated[_GroupUserPathParams, Depends()],
 ):
     """
     Removes a user from an organization group
@@ -157,8 +157,8 @@ async def delete_group_user(
     response_model=Envelope[dict[str, Any]],
 )
 async def get_group_classifiers(
-    _p: Annotated[_GroupPathParams, Depends()],
-    _q: Annotated[_ClassifiersQuery, Depends()],
+    _path: Annotated[_GroupPathParams, Depends()],
+    _query: Annotated[_ClassifiersQuery, Depends()],
 ):
     ...
 
diff --git a/api/specs/web-server/_projects_crud.py b/api/specs/web-server/_projects_crud.py
index 4c560464eb8..31f26d6425e 100644
--- a/api/specs/web-server/_projects_crud.py
+++ b/api/specs/web-server/_projects_crud.py
@@ -11,7 +11,8 @@
 
 from typing import Annotated
 
-from fastapi import APIRouter, Depends, Header, Query, status
+from _common import as_query
+from fastapi import APIRouter, Depends, Header, status
 from models_library.api_schemas_directorv2.dynamic_services import (
     GetProjectInactivityResponse,
 )
@@ -27,14 +28,14 @@
 from models_library.projects import ProjectID
 from models_library.projects_nodes_io import NodeID
 from models_library.rest_pagination import Page
-from pydantic import Json
+from pydantic import BaseModel
 from simcore_service_webserver._meta import API_VTAG
 from simcore_service_webserver.projects._common_models import ProjectPathParams
 from simcore_service_webserver.projects._crud_handlers import ProjectCreateParams
 from simcore_service_webserver.projects._crud_handlers_models import (
-    ProjectFilters,
-    ProjectListFullSearchParams,
-    ProjectListParams,
+    ProjectActiveQueryParams,
+    ProjectsListQueryParams,
+    ProjectsSearchQueryParams,
 )
 
 router = APIRouter(
@@ -45,28 +46,34 @@
 )
 
 
-@router.post(
-    "/projects",
-    response_model=Envelope[TaskGet],
-    summary="Creates a new project or copies an existing one",
-    status_code=status.HTTP_201_CREATED,
-)
-async def create_project(
-    _params: Annotated[ProjectCreateParams, Depends()],
-    _create: ProjectCreateNew | ProjectCopyOverride,
-    x_simcore_user_agent: Annotated[str | None, Header()] = "undefined",
+class _ProjectCreateHeaderParams(BaseModel):
+    x_simcore_user_agent: Annotated[
+        str | None, Header(description="Optional simcore user agent")
+    ] = "undefined"
     x_simcore_parent_project_uuid: Annotated[
         ProjectID | None,
         Header(
             description="Optionally sets a parent project UUID (both project and node must be set)",
         ),
-    ] = None,
+    ] = None
     x_simcore_parent_node_id: Annotated[
         NodeID | None,
         Header(
             description="Optionally sets a parent node ID (both project and node must be set)",
         ),
-    ] = None,
+    ] = None
+
+
+@router.post(
+    "/projects",
+    response_model=Envelope[TaskGet],
+    summary="Creates a new project or copies an existing one",
+    status_code=status.HTTP_201_CREATED,
+)
+async def create_project(
+    _h: Annotated[_ProjectCreateHeaderParams, Depends()],
+    _path: Annotated[ProjectCreateParams, Depends()],
+    _body: ProjectCreateNew | ProjectCopyOverride,
 ):
     ...
 
@@ -76,18 +83,7 @@ async def create_project(
     response_model=Page[ProjectListItem],
 )
 async def list_projects(
-    _params: Annotated[ProjectListParams, Depends()],
-    order_by: Annotated[
-        Json,
-        Query(
-            description="Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) and direction (asc|desc). The default sorting order is ascending.",
-            example='{"field": "last_change_date", "direction": "desc"}',
-        ),
-    ] = '{"field": "last_change_date", "direction": "desc"}',
-    filters: Annotated[
-        Json | None,
-        Query(description=ProjectFilters.schema_json(indent=1)),
-    ] = None,
+    _query: Annotated[as_query(ProjectsListQueryParams), Depends()],
 ):
     ...
 
@@ -96,7 +92,9 @@ async def list_projects(
     "/projects/active",
     response_model=Envelope[ProjectGet],
 )
-async def get_active_project(client_session_id: str):
+async def get_active_project(
+    _query: Annotated[ProjectActiveQueryParams, Depends()],
+):
     ...
 
 
@@ -104,7 +102,9 @@ async def get_active_project(client_session_id: str):
     "/projects/{project_id}",
     response_model=Envelope[ProjectGet],
 )
-async def get_project(project_id: ProjectID):
+async def get_project(
+    _path: Annotated[ProjectPathParams, Depends()],
+):
     ...
 
 
@@ -113,7 +113,10 @@ async def get_project(project_id: ProjectID):
     response_model=None,
     status_code=status.HTTP_204_NO_CONTENT,
 )
-async def patch_project(project_id: ProjectID, _new: ProjectPatch):
+async def patch_project(
+    _path: Annotated[ProjectPathParams, Depends()],
+    _body: ProjectPatch,
+):
     ...
 
 
@@ -121,7 +124,9 @@ async def patch_project(project_id: ProjectID, _new: ProjectPatch):
     "/projects/{project_id}",
     status_code=status.HTTP_204_NO_CONTENT,
 )
-async def delete_project(project_id: ProjectID):
+async def delete_project(
+    _path: Annotated[ProjectPathParams, Depends()],
+):
     ...
 
 
@@ -131,24 +136,17 @@ async def delete_project(project_id: ProjectID):
     status_code=status.HTTP_201_CREATED,
 )
 async def clone_project(
-    _params: Annotated[ProjectPathParams, Depends()],
+    _path: Annotated[ProjectPathParams, Depends()],
 ):
     ...
 
 
 @router.get(
     "/projects:search",
-    response_model=Page[ProjectListFullSearchParams],
+    response_model=Page[ProjectListItem],
 )
 async def list_projects_full_search(
-    _params: Annotated[ProjectListFullSearchParams, Depends()],
-    order_by: Annotated[
-        Json,
-        Query(
-            description="Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) and direction (asc|desc). The default sorting order is ascending.",
-            example='{"field": "last_change_date", "direction": "desc"}',
-        ),
-    ] = ('{"field": "last_change_date", "direction": "desc"}',),
+    _query: Annotated[as_query(ProjectsSearchQueryParams), Depends()],
 ):
     ...
 
@@ -159,6 +157,6 @@ async def list_projects_full_search(
     status_code=status.HTTP_200_OK,
 )
 async def get_project_inactivity(
-    _params: Annotated[ProjectPathParams, Depends()],
+    _path: Annotated[ProjectPathParams, Depends()],
 ):
     ...
diff --git a/api/specs/web-server/_resource_usage.py b/api/specs/web-server/_resource_usage.py
index 54924473746..2f9b1213b04 100644
--- a/api/specs/web-server/_resource_usage.py
+++ b/api/specs/web-server/_resource_usage.py
@@ -11,8 +11,8 @@
 
 from typing import Annotated
 
-from _common import assert_handler_signature_against_model
-from fastapi import APIRouter, Query, status
+from _common import as_query
+from fastapi import APIRouter, Depends, status
 from models_library.api_schemas_resource_usage_tracker.service_runs import (
     OsparcCreditsAggregatedByServiceGet,
 )
@@ -29,92 +29,49 @@
     UpdatePricingUnitBodyParams,
 )
 from models_library.generics import Envelope
-from models_library.resource_tracker import (
-    PricingPlanId,
-    PricingUnitId,
-    ServicesAggregatedUsagesTimePeriod,
-    ServicesAggregatedUsagesType,
-)
-from models_library.rest_pagination import DEFAULT_NUMBER_OF_ITEMS_PER_PAGE
-from models_library.wallets import WalletID
-from pydantic import Json, NonNegativeInt
 from simcore_service_webserver._meta import API_VTAG
 from simcore_service_webserver.resource_usage._pricing_plans_admin_handlers import (
-    _GetPricingPlanPathParams,
-    _GetPricingUnitPathParams,
+    PricingPlanGetPathParams,
+    PricingUnitGetPathParams,
 )
 from simcore_service_webserver.resource_usage._pricing_plans_handlers import (
-    _GetPricingPlanUnitPathParams,
+    PricingPlanUnitGetPathParams,
 )
 from simcore_service_webserver.resource_usage._service_runs_handlers import (
-    ORDER_BY_DESCRIPTION,
-    _ListServicesAggregatedUsagesQueryParams,
-    _ListServicesResourceUsagesQueryParams,
-    _ListServicesResourceUsagesQueryParamsWithPagination,
+    ServicesAggregatedUsagesListQueryParams,
+    ServicesResourceUsagesListQueryParams,
+    ServicesResourceUsagesReportQueryParams,
 )
 
 router = APIRouter(prefix=f"/{API_VTAG}")
 
 
-#
-# API entrypoints
-#
-
-
 @router.get(
     "/services/-/resource-usages",
     response_model=Envelope[list[ServiceRunGet]],
-    summary="Retrieve finished and currently running user services (user and product are taken from context, optionally wallet_id parameter might be provided).",
+    summary="Retrieve finished and currently running user services"
+    " (user and product are taken from context, optionally wallet_id parameter might be provided).",
     tags=["usage"],
 )
 async def list_resource_usage_services(
-    order_by: Annotated[
-        Json,
-        Query(
-            description="Order by field (wallet_id|wallet_name|user_id|project_id|project_name|node_id|node_name|service_key|service_version|service_type|started_at|stopped_at|service_run_status|credit_cost|transaction_status) and direction (asc|desc). The default sorting order is ascending.",
-            example='{"field": "started_at", "direction": "desc"}',
-        ),
-    ] = '{"field": "started_at", "direction": "desc"}',
-    filters: Annotated[
-        Json | None,
-        Query(
-            description="Filters to process on the resource usages list, encoded as JSON. Currently supports the filtering of 'started_at' field with 'from' and 'until' parameters in <yyyy-mm-dd> ISO 8601 format. The date range specified is inclusive.",
-            example='{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}',
-        ),
-    ] = None,
-    wallet_id: Annotated[WalletID | None, Query] = None,
-    limit: int = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
-    offset: NonNegativeInt = 0,
+    _query: Annotated[as_query(ServicesResourceUsagesListQueryParams), Depends()],
 ):
     ...
 
 
-assert_handler_signature_against_model(
-    list_resource_usage_services, _ListServicesResourceUsagesQueryParamsWithPagination
-)
-
-
 @router.get(
     "/services/-/aggregated-usages",
     response_model=Envelope[list[OsparcCreditsAggregatedByServiceGet]],
-    summary="Used credits based on aggregate by type, currently supported `services`. (user and product are taken from context, optionally wallet_id parameter might be provided).",
+    summary="Used credits based on aggregate by type, currently supported `services`"
+    ". (user and product are taken from context, optionally wallet_id parameter might be provided).",
     tags=["usage"],
 )
 async def list_osparc_credits_aggregated_usages(
-    aggregated_by: ServicesAggregatedUsagesType,
-    time_period: ServicesAggregatedUsagesTimePeriod,
-    wallet_id: Annotated[WalletID, Query],
-    limit: int = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
-    offset: NonNegativeInt = 0,
+    _query: Annotated[as_query(ServicesAggregatedUsagesListQueryParams), Depends()]
 ):
     ...
 
 
-assert_handler_signature_against_model(
-    list_osparc_credits_aggregated_usages, _ListServicesAggregatedUsagesQueryParams
-)
-
-
 @router.get(
     "/services/-/usage-report",
     status_code=status.HTTP_302_FOUND,
@@ -124,33 +81,15 @@ async def list_osparc_credits_aggregated_usages(
         }
     },
     tags=["usage"],
-    summary="Redirects to download CSV link. CSV obtains finished and currently running user services (user and product are taken from context, optionally wallet_id parameter might be provided).",
+    summary="Redirects to download CSV link. CSV obtains finished and currently running "
+    "user services (user and product are taken from context, optionally wallet_id parameter might be provided).",
 )
 async def export_resource_usage_services(
-    order_by: Annotated[
-        Json,
-        Query(
-            description="",
-            example='{"field": "started_at", "direction": "desc"}',
-        ),
-    ] = '{"field": "started_at", "direction": "desc"}',
-    filters: Annotated[
-        Json | None,
-        Query(
-            description=ORDER_BY_DESCRIPTION,
-            example='{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}',
-        ),
-    ] = None,
-    wallet_id: Annotated[WalletID | None, Query] = None,
+    _query: Annotated[as_query(ServicesResourceUsagesReportQueryParams), Depends()]
 ):
     ...
 
 
-assert_handler_signature_against_model(
-    list_resource_usage_services, _ListServicesResourceUsagesQueryParams
-)
-
-
 @router.get(
     "/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}",
     response_model=Envelope[PricingUnitGet],
@@ -158,16 +97,11 @@ async def export_resource_usage_services(
     tags=["pricing-plans"],
 )
 async def get_pricing_plan_unit(
-    pricing_plan_id: PricingPlanId, pricing_unit_id: PricingUnitId
+    _path: Annotated[PricingPlanUnitGetPathParams, Depends()],
 ):
     ...
 
 
-assert_handler_signature_against_model(
-    get_pricing_plan_unit, _GetPricingPlanUnitPathParams
-)
-
-
 ## Pricing plans for Admin panel
 
 
@@ -189,21 +123,20 @@ async def list_pricing_plans():
     tags=["admin"],
 )
 async def get_pricing_plan(
-    pricing_plan_id: PricingPlanId,
+    _path: Annotated[PricingPlanGetPathParams, Depends()],
 ):
     ...
 
 
-assert_handler_signature_against_model(get_pricing_plan, _GetPricingPlanPathParams)
-
-
 @router.post(
     "/admin/pricing-plans",
     response_model=Envelope[PricingPlanAdminGet],
     summary="Create pricing plan",
     tags=["admin"],
 )
-async def create_pricing_plan(body: CreatePricingPlanBodyParams):
+async def create_pricing_plan(
+    _body: CreatePricingPlanBodyParams,
+):
     ...
 
 
@@ -214,14 +147,12 @@ async def create_pricing_plan(body: CreatePricingPlanBodyParams):
     tags=["admin"],
 )
 async def update_pricing_plan(
-    pricing_plan_id: PricingPlanId, body: UpdatePricingPlanBodyParams
+    _path: Annotated[PricingPlanGetPathParams, Depends()],
+    _body: UpdatePricingPlanBodyParams,
 ):
     ...
 
 
-assert_handler_signature_against_model(update_pricing_plan, _GetPricingPlanPathParams)
-
-
 ## Pricing units for Admin panel
 
 
@@ -232,14 +163,11 @@ async def update_pricing_plan(
     tags=["admin"],
 )
 async def get_pricing_unit(
-    pricing_plan_id: PricingPlanId, pricing_unit_id: PricingUnitId
+    _path: Annotated[PricingUnitGetPathParams, Depends()],
 ):
     ...
 
 
-assert_handler_signature_against_model(get_pricing_unit, _GetPricingUnitPathParams)
-
-
 @router.post(
     "/admin/pricing-plans/{pricing_plan_id}/pricing-units",
     response_model=Envelope[PricingUnitAdminGet],
@@ -247,14 +175,12 @@ async def get_pricing_unit(
     tags=["admin"],
 )
 async def create_pricing_unit(
-    pricing_plan_id: PricingPlanId, body: CreatePricingUnitBodyParams
+    _path: Annotated[PricingPlanGetPathParams, Depends()],
+    _body: CreatePricingUnitBodyParams,
 ):
     ...
 
 
-assert_handler_signature_against_model(create_pricing_unit, _GetPricingPlanPathParams)
-
-
 @router.put(
     "/admin/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}",
     response_model=Envelope[PricingUnitAdminGet],
@@ -262,16 +188,12 @@ async def create_pricing_unit(
     tags=["admin"],
 )
 async def update_pricing_unit(
-    pricing_plan_id: PricingPlanId,
-    pricing_unit_id: PricingUnitId,
-    body: UpdatePricingUnitBodyParams,
+    _path: Annotated[PricingUnitGetPathParams, Depends()],
+    _body: UpdatePricingUnitBodyParams,
 ):
     ...
 
 
-assert_handler_signature_against_model(update_pricing_unit, _GetPricingUnitPathParams)
-
-
 ## Pricing Plans to Service Admin panel
 
 
@@ -282,14 +204,11 @@ async def update_pricing_unit(
     tags=["admin"],
 )
 async def list_connected_services_to_pricing_plan(
-    pricing_plan_id: PricingPlanId,
+    _path: Annotated[PricingPlanGetPathParams, Depends()],
 ):
     ...
 
 
-assert_handler_signature_against_model(update_pricing_unit, _GetPricingPlanPathParams)
-
-
 @router.post(
     "/admin/pricing-plans/{pricing_plan_id}/billable-services",
     response_model=Envelope[PricingPlanToServiceAdminGet],
@@ -297,10 +216,7 @@ async def list_connected_services_to_pricing_plan(
     tags=["admin"],
 )
 async def connect_service_to_pricing_plan(
-    pricing_plan_id: PricingPlanId,
-    body: ConnectServiceToPricingPlanBodyParams,
+    _path: Annotated[PricingPlanGetPathParams, Depends()],
+    _body: ConnectServiceToPricingPlanBodyParams,
 ):
     ...
-
-
-assert_handler_signature_against_model(update_pricing_unit, _GetPricingPlanPathParams)
diff --git a/api/specs/web-server/_trash.py b/api/specs/web-server/_trash.py
index cdd883f7cf3..9aa23b8b288 100644
--- a/api/specs/web-server/_trash.py
+++ b/api/specs/web-server/_trash.py
@@ -48,8 +48,8 @@ def empty_trash():
     },
 )
 def trash_project(
-    _p: Annotated[ProjectPathParams, Depends()],
-    _q: Annotated[RemoveQueryParams, Depends()],
+    _path: Annotated[ProjectPathParams, Depends()],
+    _query: Annotated[RemoveQueryParams, Depends()],
 ):
     ...
 
@@ -60,7 +60,7 @@ def trash_project(
     status_code=status.HTTP_204_NO_CONTENT,
 )
 def untrash_project(
-    _p: Annotated[ProjectPathParams, Depends()],
+    _path: Annotated[ProjectPathParams, Depends()],
 ):
     ...
 
@@ -81,8 +81,8 @@ def untrash_project(
     },
 )
 def trash_folder(
-    _p: Annotated[FoldersPathParams, Depends()],
-    _q: Annotated[RemoveQueryParams_duplicated, Depends()],
+    _path: Annotated[FoldersPathParams, Depends()],
+    _query: Annotated[RemoveQueryParams_duplicated, Depends()],
 ):
     ...
 
@@ -93,6 +93,6 @@ def trash_folder(
     status_code=status.HTTP_204_NO_CONTENT,
 )
 def untrash_folder(
-    _p: Annotated[FoldersPathParams, Depends()],
+    _path: Annotated[FoldersPathParams, Depends()],
 ):
     ...
diff --git a/packages/models-library/src/models_library/rest_base.py b/packages/models-library/src/models_library/rest_base.py
new file mode 100644
index 00000000000..a6b24ef6382
--- /dev/null
+++ b/packages/models-library/src/models_library/rest_base.py
@@ -0,0 +1,19 @@
+from pydantic import BaseModel, Extra
+
+
+class RequestParameters(BaseModel):
+    """
+    Base model for any type of request parameters,
+    i.e. context, path, query, headers
+    """
+
+    def as_params(self, **export_options) -> dict[str, str]:
+        data = self.dict(**export_options)
+        return {k: f"{v}" for k, v in data.items()}
+
+
+class StrictRequestParameters(RequestParameters):
+    """Use a base class for context, path and query parameters"""
+
+    class Config:
+        extra = Extra.forbid  # strict
diff --git a/packages/models-library/src/models_library/rest_ordering.py b/packages/models-library/src/models_library/rest_ordering.py
index c8a791343ee..31a59e984bd 100644
--- a/packages/models-library/src/models_library/rest_ordering.py
+++ b/packages/models-library/src/models_library/rest_ordering.py
@@ -1,8 +1,12 @@
 from enum import Enum
+from typing import Any, ClassVar
 
-from pydantic import BaseModel, Field
+from models_library.utils.json_serialization import json_dumps
+from pydantic import BaseModel, Extra, Field, validator
 
 from .basic_types import IDStr
+from .rest_base import RequestParameters
+from .utils.common_validators import parse_json_pre_validator
 
 
 class OrderDirection(str, Enum):
@@ -11,10 +15,100 @@ class OrderDirection(str, Enum):
 
 
 class OrderBy(BaseModel):
-    """inspired by Google AIP https://google.aip.dev/132#ordering"""
+    # Based on https://google.aip.dev/132#ordering
+    field: IDStr = Field(..., description="field name identifier")
+    direction: OrderDirection = Field(
+        default=OrderDirection.ASC,
+        description=(
+            f"As [A,B,C,...] if `{OrderDirection.ASC.value}`"
+            f" or [Z,Y,X, ...] if `{OrderDirection.DESC.value}`"
+        ),
+    )
 
-    field: IDStr = Field()
-    direction: OrderDirection = Field(default=OrderDirection.ASC)
 
-    class Config:
-        extra = "forbid"
+class _BaseOrderQueryParams(RequestParameters):
+    order_by: OrderBy | None = None
+
+
+def create_ordering_query_model_classes(
+    *,
+    ordering_fields: set[str],
+    default: OrderBy,
+    ordering_fields_api_to_column_map: dict[str, str] | None = None,
+) -> type[_BaseOrderQueryParams]:
+    """Factory to create an uniform model used as ordering parameters in a query
+
+    Arguments:
+        ordering_fields -- A set of valid fields that can be used for ordering.
+            These should correspond to API field names.
+        default -- The default ordering configuration to be applied if no explicit
+            ordering is provided
+
+    Keyword Arguments:
+        ordering_fields_api_to_column_map -- A mapping of API field names to
+            database column names. If provided, fields specified in the API
+            will be automatically translated to their corresponding database
+            column names for seamless integration with database queries.
+    """
+    _ordering_fields_api_to_column_map = ordering_fields_api_to_column_map or {}
+
+    assert set(_ordering_fields_api_to_column_map.keys()).issubset(  # nosec
+        ordering_fields
+    )
+
+    assert default.field in ordering_fields  # nosec
+
+    msg_field_options = "|".join(sorted(ordering_fields))
+    msg_direction_options = "|".join(sorted(OrderDirection))
+
+    class _OrderBy(OrderBy):
+        class Config:
+            schema_extra: ClassVar[dict[str, Any]] = {
+                "example": {
+                    "field": next(iter(ordering_fields)),
+                    "direction": OrderDirection.DESC.value,
+                }
+            }
+            extra = Extra.forbid
+            # Necessary to run _check_ordering_field_and_map in defaults and assignments
+            validate_all = True
+            validate_assignment = True
+
+        @validator("field", allow_reuse=True, always=True)
+        @classmethod
+        def _check_ordering_field_and_map(cls, v):
+            if v not in ordering_fields:
+                msg = (
+                    f"We do not support ordering by provided field '{v}'. "
+                    f"Fields supported are {msg_field_options}."
+                )
+                raise ValueError(msg)
+
+            # API field name -> DB column_name conversion
+            return _ordering_fields_api_to_column_map.get(v) or v
+
+    order_by_example: dict[str, Any] = _OrderBy.Config.schema_extra["example"]
+    order_by_example_json = json_dumps(order_by_example)
+    assert _OrderBy.parse_obj(order_by_example), "Example is invalid"  # nosec
+
+    converted_default = _OrderBy.parse_obj(
+        # NOTE: enforces ordering_fields_api_to_column_map
+        default.dict()
+    )
+
+    class _OrderQueryParams(_BaseOrderQueryParams):
+        order_by: _OrderBy = Field(
+            default=converted_default,
+            description=(
+                f"Order by field (`{msg_field_options}`) and direction (`{msg_direction_options}`). "
+                f"The default sorting order is `{json_dumps(default)}`."
+            ),
+            example=order_by_example,
+            example_json=order_by_example_json,
+        )
+
+        _pre_parse_string = validator("order_by", allow_reuse=True, pre=True)(
+            parse_json_pre_validator
+        )
+
+    return _OrderQueryParams
diff --git a/packages/models-library/src/models_library/rest_pagination.py b/packages/models-library/src/models_library/rest_pagination.py
index 89c90cb1c2d..0213fb4f8a5 100644
--- a/packages/models-library/src/models_library/rest_pagination.py
+++ b/packages/models-library/src/models_library/rest_pagination.py
@@ -13,6 +13,7 @@
 )
 from pydantic.generics import GenericModel
 
+from .rest_base import RequestParameters
 from .utils.common_validators import none_to_empty_list_pre_validator
 
 # Default limit values
@@ -29,7 +30,7 @@ class PageLimitInt(ConstrainedInt):
 DEFAULT_NUMBER_OF_ITEMS_PER_PAGE: Final[PageLimitInt] = parse_obj_as(PageLimitInt, 20)
 
 
-class PageQueryParameters(BaseModel):
+class PageQueryParameters(RequestParameters):
     """Use as pagination options in query parameters"""
 
     limit: PageLimitInt = Field(
diff --git a/packages/models-library/src/models_library/utils/common_validators.py b/packages/models-library/src/models_library/utils/common_validators.py
index f1d754de5dc..0fcf1879951 100644
--- a/packages/models-library/src/models_library/utils/common_validators.py
+++ b/packages/models-library/src/models_library/utils/common_validators.py
@@ -20,6 +20,10 @@ class MyModel(BaseModel):
 import operator
 from typing import Any
 
+from orjson import JSONDecodeError
+
+from .json_serialization import json_loads
+
 
 def empty_str_to_none_pre_validator(value: Any):
     if isinstance(value, str) and value.strip() == "":
@@ -39,6 +43,16 @@ def none_to_empty_list_pre_validator(value: Any):
     return value
 
 
+def parse_json_pre_validator(value: Any):
+    if isinstance(value, str):
+        try:
+            return json_loads(value)
+        except JSONDecodeError as err:
+            msg = f"Invalid JSON {value=}: {err}"
+            raise TypeError(msg) from err
+    return value
+
+
 def create_enums_pre_validator(enum_cls: type[enum.Enum]):
     """Enables parsing enums from equivalent enums
 
diff --git a/packages/models-library/tests/test_rest_ordering.py b/packages/models-library/tests/test_rest_ordering.py
new file mode 100644
index 00000000000..fec004cd01e
--- /dev/null
+++ b/packages/models-library/tests/test_rest_ordering.py
@@ -0,0 +1,139 @@
+import pytest
+from models_library.basic_types import IDStr
+from models_library.rest_ordering import (
+    OrderBy,
+    OrderDirection,
+    create_ordering_query_model_classes,
+)
+from models_library.utils.json_serialization import json_dumps
+from pydantic import BaseModel, Extra, Field, Json, ValidationError, validator
+
+
+class ReferenceOrderQueryParamsClass(BaseModel):
+    # NOTE: this class is a copy of `FolderListSortParams` from
+    # services/web/server/src/simcore_service_webserver/folders/_models.py
+    # and used as a reference in these tests to ensure the same functionality
+
+    # pylint: disable=unsubscriptable-object
+    order_by: Json[OrderBy] = Field(
+        default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
+        description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
+        example='{"field": "name", "direction": "desc"}',
+    )
+
+    @validator("order_by", check_fields=False)
+    @classmethod
+    def _validate_order_by_field(cls, v):
+        if v.field not in {
+            "modified_at",
+            "name",
+            "description",
+        }:
+            msg = f"We do not support ordering by provided field {v.field}"
+            raise ValueError(msg)
+        if v.field == "modified_at":
+            v.field = "modified_column"
+        return v
+
+    class Config:
+        extra = Extra.forbid
+
+
+def test_ordering_query_model_class_factory():
+    BaseOrderingQueryModel = create_ordering_query_model_classes(
+        ordering_fields={"modified_at", "name", "description"},
+        default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
+        ordering_fields_api_to_column_map={"modified_at": "modified_column"},
+    )
+
+    # inherits to add extra post-validator
+    class OrderQueryParamsModel(BaseOrderingQueryModel):
+        ...
+
+    # normal
+    data = {"order_by": {"field": "modified_at", "direction": "asc"}}
+    model = OrderQueryParamsModel.parse_obj(data)
+
+    assert model.order_by
+    assert model.order_by.dict() == {"field": "modified_column", "direction": "asc"}
+
+    # test against reference
+    expected = ReferenceOrderQueryParamsClass.parse_obj(
+        {"order_by": json_dumps({"field": "modified_at", "direction": "asc"})}
+    )
+    assert expected.dict() == model.dict()
+
+
+def test_ordering_query_model_class__fails_with_invalid_fields():
+
+    OrderQueryParamsModel = create_ordering_query_model_classes(
+        ordering_fields={"modified", "name", "description"},
+        default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
+    )
+
+    # fails with invalid field to sort
+    with pytest.raises(ValidationError) as err_info:
+        OrderQueryParamsModel.parse_obj({"order_by": {"field": "INVALID"}})
+
+    error = err_info.value.errors()[0]
+
+    assert error["type"] == "value_error"
+    assert "INVALID" in error["msg"]
+    assert error["loc"] == ("order_by", "field")
+
+
+def test_ordering_query_model_class__fails_with_invalid_direction():
+    OrderQueryParamsModel = create_ordering_query_model_classes(
+        ordering_fields={"modified", "name", "description"},
+        default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
+    )
+
+    with pytest.raises(ValidationError) as err_info:
+        OrderQueryParamsModel.parse_obj(
+            {"order_by": {"field": "modified", "direction": "INVALID"}}
+        )
+
+    error = err_info.value.errors()[0]
+
+    assert error["type"] == "type_error.enum"
+    assert error["loc"] == ("order_by", "direction")
+
+
+def test_ordering_query_model_class__defaults():
+
+    OrderQueryParamsModel = create_ordering_query_model_classes(
+        ordering_fields={"modified", "name", "description"},
+        default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
+        ordering_fields_api_to_column_map={"modified": "modified_at"},
+    )
+
+    # checks  all defaults
+    model = OrderQueryParamsModel()
+    assert model.order_by
+    assert model.order_by.field == "modified_at"  # NOTE that this was mapped!
+    assert model.order_by.direction == OrderDirection.DESC
+
+    # partial defaults
+    model = OrderQueryParamsModel.parse_obj({"order_by": {"field": "name"}})
+    assert model.order_by
+    assert model.order_by.field == "name"
+    assert model.order_by.direction == OrderBy.__fields__["direction"].default
+
+    # direction alone is invalid
+    with pytest.raises(ValidationError) as err_info:
+        OrderQueryParamsModel.parse_obj({"order_by": {"direction": "asc"}})
+
+    error = err_info.value.errors()[0]
+    assert error["loc"] == ("order_by", "field")
+    assert error["type"] == "value_error.missing"
+
+
+def test_ordering_query_model_with_map():
+    OrderQueryParamsModel = create_ordering_query_model_classes(
+        ordering_fields={"modified", "name", "description"},
+        default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
+        ordering_fields_api_to_column_map={"modified": "some_db_column_name"},
+    )
+
+    model = OrderQueryParamsModel.parse_obj({"order_by": {"field": "modified"}})
+    assert model.order_by.field == "some_db_column_name"
diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py b/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py
index 2f71de33e25..3ce30f4131a 100644
--- a/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py
+++ b/packages/pytest-simcore/src/pytest_simcore/helpers/assert_checks.py
@@ -82,13 +82,13 @@ def _do_assert_error(
 
     assert is_error(expected_status_code)
 
-    assert len(error["errors"]) == 1
-
-    err = error["errors"][0]
+    assert len(error["errors"]) >= 1
     if expected_msg:
-        assert expected_msg in err["message"]
+        messages = [detail["message"] for detail in error["errors"]]
+        assert expected_msg in messages
 
     if expected_error_code:
-        assert expected_error_code == err["code"]
+        codes = [detail["code"] for detail in error["errors"]]
+        assert expected_error_code in codes
 
     return data, error
diff --git a/packages/service-library/src/servicelib/aiohttp/requests_validation.py b/packages/service-library/src/servicelib/aiohttp/requests_validation.py
index 085243c5d26..e5cef8ecd96 100644
--- a/packages/service-library/src/servicelib/aiohttp/requests_validation.py
+++ b/packages/service-library/src/servicelib/aiohttp/requests_validation.py
@@ -14,7 +14,7 @@
 
 from aiohttp import web
 from models_library.utils.json_serialization import json_dumps
-from pydantic import BaseModel, Extra, ValidationError, parse_obj_as
+from pydantic import BaseModel, ValidationError, parse_obj_as
 
 from ..mimetype_constants import MIMETYPE_APPLICATION_JSON
 from . import status
@@ -24,17 +24,6 @@
 UnionOfModelTypes: TypeAlias = Union[type[ModelClass], type[ModelClass]]  # noqa: UP007
 
 
-class RequestParams(BaseModel):
-    ...
-
-
-class StrictRequestParams(BaseModel):
-    """Use a base class for context, path and query parameters"""
-
-    class Config:
-        extra = Extra.forbid  # strict
-
-
 @contextmanager
 def handle_validation_as_http_error(
     *, error_msg_template: str, resource_name: str, use_error_v1: bool
diff --git a/packages/service-library/src/servicelib/fastapi/openapi.py b/packages/service-library/src/servicelib/fastapi/openapi.py
index 37e21c13278..dc01e2452b1 100644
--- a/packages/service-library/src/servicelib/fastapi/openapi.py
+++ b/packages/service-library/src/servicelib/fastapi/openapi.py
@@ -25,7 +25,7 @@
 }
 
 
-def get_common_oas_options(is_devel_mode: bool) -> dict[str, Any]:
+def get_common_oas_options(*, is_devel_mode: bool) -> dict[str, Any]:
     """common OAS options for FastAPI constructor"""
     servers: list[dict[str, Any]] = [
         _OAS_DEFAULT_SERVER,
diff --git a/packages/service-library/tests/aiohttp/test_requests_validation.py b/packages/service-library/tests/aiohttp/test_requests_validation.py
index 08e2f07bfbe..003f363f6e2 100644
--- a/packages/service-library/tests/aiohttp/test_requests_validation.py
+++ b/packages/service-library/tests/aiohttp/test_requests_validation.py
@@ -3,15 +3,21 @@
 # pylint: disable=unused-variable
 
 import json
-from typing import Callable
+from collections.abc import Callable
 from uuid import UUID
 
 import pytest
 from aiohttp import web
-from aiohttp.test_utils import TestClient
+from aiohttp.test_utils import TestClient, make_mocked_request
 from faker import Faker
+from models_library.rest_base import RequestParameters, StrictRequestParameters
+from models_library.rest_ordering import (
+    OrderBy,
+    OrderDirection,
+    create_ordering_query_model_classes,
+)
 from models_library.utils.json_serialization import json_dumps
-from pydantic import BaseModel, Extra, Field
+from pydantic import BaseModel, Field
 from servicelib.aiohttp import status
 from servicelib.aiohttp.requests_validation import (
     parse_request_body_as,
@@ -19,6 +25,7 @@
     parse_request_path_parameters_as,
     parse_request_query_parameters_as,
 )
+from yarl import URL
 
 RQT_USERID_KEY = f"{__name__}.user_id"
 APP_SECRET_KEY = f"{__name__}.secret"
@@ -30,7 +37,7 @@ def jsonable_encoder(data):
     return json.loads(json_dumps(data))
 
 
-class MyRequestContext(BaseModel):
+class MyRequestContext(RequestParameters):
     user_id: int = Field(alias=RQT_USERID_KEY)
     secret: str = Field(alias=APP_SECRET_KEY)
 
@@ -39,31 +46,24 @@ def create_fake(cls, faker: Faker):
         return cls(user_id=faker.pyint(), secret=faker.password())
 
 
-class MyRequestPathParams(BaseModel):
+class MyRequestPathParams(StrictRequestParameters):
     project_uuid: UUID
 
-    class Config:
-        extra = Extra.forbid
-
     @classmethod
     def create_fake(cls, faker: Faker):
         return cls(project_uuid=faker.uuid4())
 
 
-class MyRequestQueryParams(BaseModel):
+class MyRequestQueryParams(RequestParameters):
     is_ok: bool = True
     label: str
 
-    def as_params(self, **kwargs) -> dict[str, str]:
-        data = self.dict(**kwargs)
-        return {k: f"{v}" for k, v in data.items()}
-
     @classmethod
     def create_fake(cls, faker: Faker):
         return cls(is_ok=faker.pybool(), label=faker.word())
 
 
-class MyRequestHeadersParams(BaseModel):
+class MyRequestHeadersParams(RequestParameters):
     user_agent: str = Field(alias="X-Simcore-User-Agent")
     optional_header: str | None = Field(default=None, alias="X-Simcore-Optional-Header")
 
@@ -359,3 +359,19 @@ async def test_parse_request_with_invalid_headers_params(
             ],
         }
     }
+
+
+def test_parse_request_query_parameters_as_with_order_by_query_models():
+
+    OrderQueryModel = create_ordering_query_model_classes(
+        ordering_fields={"modified", "name"}, default=OrderBy(field="name")
+    )
+
+    expected = OrderBy(field="name", direction=OrderDirection.ASC)
+
+    url = URL("/test").with_query(order_by=expected.json())
+
+    request = make_mocked_request("GET", path=f"{url}")
+
+    query_params = parse_request_query_parameters_as(OrderQueryModel, request)
+    assert query_params.order_by == expected
diff --git a/packages/service-library/tests/fastapi/test_request_decorators.py b/packages/service-library/tests/fastapi/test_request_decorators.py
index 312684437e7..18f6267cf33 100644
--- a/packages/service-library/tests/fastapi/test_request_decorators.py
+++ b/packages/service-library/tests/fastapi/test_request_decorators.py
@@ -6,9 +6,10 @@
 import subprocess
 import sys
 import time
+from collections.abc import Callable, Iterator
 from contextlib import contextmanager
 from pathlib import Path
-from typing import Callable, Iterator, NamedTuple
+from typing import NamedTuple
 
 import pytest
 import requests
diff --git a/services/agent/src/simcore_service_agent/core/application.py b/services/agent/src/simcore_service_agent/core/application.py
index 84bc71e24c5..c11ec676a17 100644
--- a/services/agent/src/simcore_service_agent/core/application.py
+++ b/services/agent/src/simcore_service_agent/core/application.py
@@ -48,7 +48,7 @@ def create_app() -> FastAPI:
         description=SUMMARY,
         version=f"{VERSION}",
         openapi_url=f"/api/{API_VTAG}/openapi.json",
-        **get_common_oas_options(settings.SC_BOOT_MODE.is_devel_mode()),
+        **get_common_oas_options(is_devel_mode=settings.SC_BOOT_MODE.is_devel_mode()),
     )
     override_fastapi_openapi_method(app)
     app.state.settings = settings
diff --git a/services/director-v2/src/simcore_service_director_v2/core/application.py b/services/director-v2/src/simcore_service_director_v2/core/application.py
index 6487d725143..621d9d93c42 100644
--- a/services/director-v2/src/simcore_service_director_v2/core/application.py
+++ b/services/director-v2/src/simcore_service_director_v2/core/application.py
@@ -132,7 +132,7 @@ def create_base_app(settings: AppSettings | None = None) -> FastAPI:
         description=SUMMARY,
         version=API_VERSION,
         openapi_url=f"/api/{API_VTAG}/openapi.json",
-        **get_common_oas_options(settings.SC_BOOT_MODE.is_devel_mode()),
+        **get_common_oas_options(is_devel_mode=settings.SC_BOOT_MODE.is_devel_mode()),
     )
     override_fastapi_openapi_method(app)
     app.state.settings = settings
diff --git a/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/core/application.py b/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/core/application.py
index 59547f40119..7e89d37d801 100644
--- a/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/core/application.py
+++ b/services/dynamic-sidecar/src/simcore_service_dynamic_sidecar/core/application.py
@@ -142,7 +142,7 @@ def create_base_app() -> FastAPI:
         description=SUMMARY,
         version=API_VERSION,
         openapi_url=f"/api/{API_VTAG}/openapi.json",
-        **get_common_oas_options(settings.SC_BOOT_MODE.is_devel_mode()),
+        **get_common_oas_options(is_devel_mode=settings.SC_BOOT_MODE.is_devel_mode()),
     )
     override_fastapi_openapi_method(app)
     app.state.settings = settings
diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml
index 860d9869218..df35af2db92 100644
--- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml
+++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml
@@ -2601,51 +2601,20 @@ paths:
       parameters:
       - required: false
         schema:
-          title: Folder Id
-          exclusiveMinimum: true
-          type: integer
-          minimum: 0
-        name: folder_id
+          title: Filters
+          type: string
+          description: Custom filter query parameter encoded as JSON
+        name: filters
         in: query
       - required: false
-        schema:
-          title: Workspace Id
-          exclusiveMinimum: true
-          type: integer
-          minimum: 0
-        name: workspace_id
-        in: query
-      - description: Order by field (modified_at|name|description) and direction (asc|desc).
-          The default sorting order is ascending.
-        required: false
         schema:
           title: Order By
-          description: Order by field (modified_at|name|description) and direction
-            (asc|desc). The default sorting order is ascending.
-          default: '{"field": "modified_at", "direction": "desc"}'
-        example: '{"field": "name", "direction": "desc"}'
-        name: order_by
-        in: query
-      - description: "{\n \"title\": \"FolderFilters\",\n \"description\": \"Encoded\
-          \ as JSON. Each available filter can have its own logic (should be well\
-          \ documented)\\nInspired by Docker API https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList.\"\
-          ,\n \"type\": \"object\",\n \"properties\": {\n  \"trashed\": {\n   \"title\"\
-          : \"Trashed\",\n   \"description\": \"Set to true to list trashed, false\
-          \ to list non-trashed (default), None to list all\",\n   \"default\": false,\n\
-          \   \"type\": \"boolean\"\n  }\n }\n}"
-        required: false
-        schema:
-          title: Filters
           type: string
-          description: "{\n \"title\": \"FolderFilters\",\n \"description\": \"Encoded\
-            \ as JSON. Each available filter can have its own logic (should be well\
-            \ documented)\\nInspired by Docker API https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList.\"\
-            ,\n \"type\": \"object\",\n \"properties\": {\n  \"trashed\": {\n   \"\
-            title\": \"Trashed\",\n   \"description\": \"Set to true to list trashed,\
-            \ false to list non-trashed (default), None to list all\",\n   \"default\"\
-            : false,\n   \"type\": \"boolean\"\n  }\n }\n}"
-          format: json-string
-        name: filters
+          description: Order by field (`description|modified|name`) and direction
+            (`asc|desc`). The default sorting order is `{"field":"modified","direction":"desc"}`.
+          default: '{"field":"modified","direction":"desc"}'
+          example: '{"field":"some_field_name","direction":"desc"}'
+        name: order_by
         in: query
       - required: false
         schema:
@@ -2665,6 +2634,22 @@ paths:
           default: 0
         name: offset
         in: query
+      - required: false
+        schema:
+          title: Folder Id
+          exclusiveMinimum: true
+          type: integer
+          minimum: 0
+        name: folder_id
+        in: query
+      - required: false
+        schema:
+          title: Workspace Id
+          exclusiveMinimum: true
+          type: integer
+          minimum: 0
+        name: workspace_id
+        in: query
       responses:
         '200':
           description: Successful Response
@@ -2699,41 +2684,20 @@ paths:
       parameters:
       - required: false
         schema:
-          title: Text
+          title: Filters
           type: string
-        name: text
+          description: Custom filter query parameter encoded as JSON
+        name: filters
         in: query
-      - description: Order by field (modified_at|name|description) and direction (asc|desc).
-          The default sorting order is ascending.
-        required: false
+      - required: false
         schema:
           title: Order By
-          description: Order by field (modified_at|name|description) and direction
-            (asc|desc). The default sorting order is ascending.
-          default: '{"field": "modified_at", "direction": "desc"}'
-        example: '{"field": "name", "direction": "desc"}'
-        name: order_by
-        in: query
-      - description: "{\n \"title\": \"FolderFilters\",\n \"description\": \"Encoded\
-          \ as JSON. Each available filter can have its own logic (should be well\
-          \ documented)\\nInspired by Docker API https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList.\"\
-          ,\n \"type\": \"object\",\n \"properties\": {\n  \"trashed\": {\n   \"title\"\
-          : \"Trashed\",\n   \"description\": \"Set to true to list trashed, false\
-          \ to list non-trashed (default), None to list all\",\n   \"default\": false,\n\
-          \   \"type\": \"boolean\"\n  }\n }\n}"
-        required: false
-        schema:
-          title: Filters
           type: string
-          description: "{\n \"title\": \"FolderFilters\",\n \"description\": \"Encoded\
-            \ as JSON. Each available filter can have its own logic (should be well\
-            \ documented)\\nInspired by Docker API https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList.\"\
-            ,\n \"type\": \"object\",\n \"properties\": {\n  \"trashed\": {\n   \"\
-            title\": \"Trashed\",\n   \"description\": \"Set to true to list trashed,\
-            \ false to list non-trashed (default), None to list all\",\n   \"default\"\
-            : false,\n   \"type\": \"boolean\"\n  }\n }\n}"
-          format: json-string
-        name: filters
+          description: Order by field (`description|modified|name`) and direction
+            (`asc|desc`). The default sorting order is `{"field":"modified","direction":"desc"}`.
+          default: '{"field":"modified","direction":"desc"}'
+          example: '{"field":"some_field_name","direction":"desc"}'
+        name: order_by
         in: query
       - required: false
         schema:
@@ -2753,6 +2717,13 @@ paths:
           default: 0
         name: offset
         in: query
+      - required: false
+        schema:
+          title: Text
+          maxLength: 100
+          type: string
+        name: text
+        in: query
       responses:
         '200':
           description: Successful Response
@@ -3136,56 +3107,6 @@ paths:
       summary: List Projects
       operationId: list_projects
       parameters:
-      - description: Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date)
-          and direction (asc|desc). The default sorting order is ascending.
-        required: false
-        schema:
-          title: Order By
-          description: Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date)
-            and direction (asc|desc). The default sorting order is ascending.
-          default: '{"field": "last_change_date", "direction": "desc"}'
-        example: '{"field": "last_change_date", "direction": "desc"}'
-        name: order_by
-        in: query
-      - description: "{\n \"title\": \"ProjectFilters\",\n \"description\": \"Encoded\
-          \ as JSON. Each available filter can have its own logic (should be well\
-          \ documented)\\nInspired by Docker API https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList.\"\
-          ,\n \"type\": \"object\",\n \"properties\": {\n  \"trashed\": {\n   \"title\"\
-          : \"Trashed\",\n   \"description\": \"Set to true to list trashed, false\
-          \ to list non-trashed (default), None to list all\",\n   \"default\": false,\n\
-          \   \"type\": \"boolean\"\n  }\n }\n}"
-        required: false
-        schema:
-          title: Filters
-          type: string
-          description: "{\n \"title\": \"ProjectFilters\",\n \"description\": \"Encoded\
-            \ as JSON. Each available filter can have its own logic (should be well\
-            \ documented)\\nInspired by Docker API https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList.\"\
-            ,\n \"type\": \"object\",\n \"properties\": {\n  \"trashed\": {\n   \"\
-            title\": \"Trashed\",\n   \"description\": \"Set to true to list trashed,\
-            \ false to list non-trashed (default), None to list all\",\n   \"default\"\
-            : false,\n   \"type\": \"boolean\"\n  }\n }\n}"
-          format: json-string
-        name: filters
-        in: query
-      - required: false
-        schema:
-          title: Limit
-          exclusiveMaximum: true
-          minimum: 1
-          type: integer
-          default: 20
-          maximum: 50
-        name: limit
-        in: query
-      - required: false
-        schema:
-          title: Offset
-          minimum: 0
-          type: integer
-          default: 0
-        name: offset
-        in: query
       - required: false
         schema:
           allOf:
@@ -3223,6 +3144,41 @@ paths:
           minimum: 0
         name: workspace_id
         in: query
+      - required: false
+        schema:
+          title: Filters
+          type: string
+          description: Custom filter query parameter encoded as JSON
+        name: filters
+        in: query
+      - required: false
+        schema:
+          title: Order By
+          type: string
+          description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`)
+            and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`.
+          default: '{"field":"last_change_date","direction":"desc"}'
+          example: '{"field":"some_field_name","direction":"desc"}'
+        name: order_by
+        in: query
+      - required: false
+        schema:
+          title: Limit
+          exclusiveMaximum: true
+          minimum: 1
+          type: integer
+          default: 20
+          maximum: 50
+        name: limit
+        in: query
+      - required: false
+        schema:
+          title: Offset
+          minimum: 0
+          type: integer
+          default: 0
+        name: offset
+        in: query
       responses:
         '200':
           description: Successful Response
@@ -3264,10 +3220,12 @@ paths:
           default: false
         name: hidden
         in: query
-      - required: false
+      - description: Optional simcore user agent
+        required: false
         schema:
           title: X-Simcore-User-Agent
           type: string
+          description: Optional simcore user agent
           default: undefined
         name: x-simcore-user-agent
         in: header
@@ -3297,7 +3255,7 @@ paths:
         content:
           application/json:
             schema:
-              title: ' Create'
+              title: ' Body'
               anyOf:
               - $ref: '#/components/schemas/ProjectCreateNew'
               - $ref: '#/components/schemas/ProjectCopyOverride'
@@ -3416,16 +3374,14 @@ paths:
       summary: List Projects Full Search
       operationId: list_projects_full_search
       parameters:
-      - description: Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date)
-          and direction (asc|desc). The default sorting order is ascending.
-        required: false
+      - required: false
         schema:
           title: Order By
-          description: Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date)
-            and direction (asc|desc). The default sorting order is ascending.
-          default:
-          - '{"field": "last_change_date", "direction": "desc"}'
-        example: '{"field": "last_change_date", "direction": "desc"}'
+          type: string
+          description: Order by field (`creation_date|description|last_change_date|name|prj_owner|type|uuid`)
+            and direction (`asc|desc`). The default sorting order is `{"field":"last_change_date","direction":"desc"}`.
+          default: '{"field":"last_change_date","direction":"desc"}'
+          example: '{"field":"some_field_name","direction":"desc"}'
         name: order_by
         in: query
       - required: false
@@ -3465,7 +3421,7 @@ paths:
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/Page_ProjectListFullSearchParams_'
+                $ref: '#/components/schemas/Page_ProjectListItem_'
   /v0/projects/{project_id}/inactivity:
     get:
       tags:
@@ -4649,22 +4605,25 @@ paths:
         are taken from context, optionally wallet_id parameter might be provided).
       operationId: list_resource_usage_services
       parameters:
-      - description: Order by field (wallet_id|wallet_name|user_id|project_id|project_name|node_id|node_name|service_key|service_version|service_type|started_at|stopped_at|service_run_status|credit_cost|transaction_status)
-          and direction (asc|desc). The default sorting order is ascending.
-        required: false
+      - required: false
         schema:
           title: Order By
-          description: Order by field (wallet_id|wallet_name|user_id|project_id|project_name|node_id|node_name|service_key|service_version|service_type|started_at|stopped_at|service_run_status|credit_cost|transaction_status)
-            and direction (asc|desc). The default sorting order is ascending.
-          default: '{"field": "started_at", "direction": "desc"}'
-        example: '{"field": "started_at", "direction": "desc"}'
+          type: string
+          description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`)
+            and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`.
+          default: '{"field":"started_at","direction":"desc"}'
+          example: '{"field":"some_field_name","direction":"desc"}'
         name: order_by
         in: query
-      - description: Filters to process on the resource usages list, encoded as JSON.
-          Currently supports the filtering of 'started_at' field with 'from' and 'until'
-          parameters in <yyyy-mm-dd> ISO 8601 format. The date range specified is
-          inclusive.
-        required: false
+      - required: false
+        schema:
+          title: Wallet Id
+          exclusiveMinimum: true
+          type: integer
+          minimum: 0
+        name: wallet_id
+        in: query
+      - required: false
         schema:
           title: Filters
           type: string
@@ -4672,23 +4631,16 @@ paths:
             JSON. Currently supports the filtering of 'started_at' field with 'from'
             and 'until' parameters in <yyyy-mm-dd> ISO 8601 format. The date range
             specified is inclusive.
-          format: json-string
-        example: '{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}'
         name: filters
         in: query
-      - required: false
-        schema:
-          title: Wallet Id
-          exclusiveMinimum: true
-          type: integer
-          minimum: 0
-        name: wallet_id
-        in: query
       - required: false
         schema:
           title: Limit
+          exclusiveMaximum: true
+          minimum: 1
           type: integer
           default: 20
+          maximum: 50
         name: limit
         in: query
       - required: false
@@ -4715,29 +4667,14 @@ paths:
         be provided).
       operationId: list_osparc_credits_aggregated_usages
       parameters:
-      - required: true
-        schema:
-          $ref: '#/components/schemas/ServicesAggregatedUsagesType'
-        name: aggregated_by
-        in: query
-      - required: true
-        schema:
-          $ref: '#/components/schemas/ServicesAggregatedUsagesTimePeriod'
-        name: time_period
-        in: query
-      - required: true
-        schema:
-          title: Wallet Id
-          exclusiveMinimum: true
-          type: integer
-          minimum: 0
-        name: wallet_id
-        in: query
       - required: false
         schema:
           title: Limit
+          exclusiveMaximum: true
+          minimum: 1
           type: integer
           default: 20
+          maximum: 50
         name: limit
         in: query
       - required: false
@@ -4748,6 +4685,24 @@ paths:
           default: 0
         name: offset
         in: query
+      - required: false
+        schema:
+          $ref: '#/components/schemas/ServicesAggregatedUsagesType'
+        name: aggregated_by
+        in: query
+      - required: false
+        schema:
+          $ref: '#/components/schemas/ServicesAggregatedUsagesTimePeriod'
+        name: time_period
+        in: query
+      - required: false
+        schema:
+          title: Wallet Id
+          exclusiveMinimum: true
+          type: integer
+          minimum: 0
+        name: wallet_id
+        in: query
       responses:
         '200':
           description: Successful Response
@@ -4767,21 +4722,12 @@ paths:
       - required: false
         schema:
           title: Order By
-          default: '{"field": "started_at", "direction": "desc"}'
-        example: '{"field": "started_at", "direction": "desc"}'
-        name: order_by
-        in: query
-      - description: Order by field (wallet_id|wallet_name|user_id|project_id|project_name|node_id|node_name|service_key|service_version|service_type|started_at|stopped_at|service_run_status|credit_cost|transaction_status)
-          and direction (asc|desc). The default sorting order is ascending.
-        required: false
-        schema:
-          title: Filters
           type: string
-          description: Order by field (wallet_id|wallet_name|user_id|project_id|project_name|node_id|node_name|service_key|service_version|service_type|started_at|stopped_at|service_run_status|credit_cost|transaction_status)
-            and direction (asc|desc). The default sorting order is ascending.
-          format: json-string
-        example: '{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}'
-        name: filters
+          description: Order by field (`credit_cost|node_id|node_name|project_id|project_name|root_parent_project_id|root_parent_project_name|service_key|service_run_status|service_type|service_version|started_at|stopped_at|transaction_status|user_email|user_id|wallet_id|wallet_name`)
+            and direction (`asc|desc`). The default sorting order is `{"field":"started_at","direction":"desc"}`.
+          default: '{"field":"started_at","direction":"desc"}'
+          example: '{"field":"some_field_name","direction":"desc"}'
+        name: order_by
         in: query
       - required: false
         schema:
@@ -4791,6 +4737,16 @@ paths:
           minimum: 0
         name: wallet_id
         in: query
+      - required: false
+        schema:
+          title: Filters
+          type: string
+          description: Filters to process on the resource usages list, encoded as
+            JSON. Currently supports the filtering of 'started_at' field with 'from'
+            and 'until' parameters in <yyyy-mm-dd> ISO 8601 format. The date range
+            specified is inclusive.
+        name: filters
+        in: query
       responses:
         '302':
           description: redirection to download link
@@ -9905,25 +9861,6 @@ components:
             $ref: '#/components/schemas/ProjectIterationResultItem'
       additionalProperties: false
       description: Paginated response model of ItemTs
-    Page_ProjectListFullSearchParams_:
-      title: Page[ProjectListFullSearchParams]
-      required:
-      - _meta
-      - _links
-      - data
-      type: object
-      properties:
-        _meta:
-          $ref: '#/components/schemas/PageMetaInfoLimitOffset'
-        _links:
-          $ref: '#/components/schemas/PageLinks'
-        data:
-          title: Data
-          type: array
-          items:
-            $ref: '#/components/schemas/ProjectListFullSearchParams'
-      additionalProperties: false
-      description: Paginated response model of ItemTs
     Page_ProjectListItem_:
       title: Page[ProjectListItem]
       required:
@@ -10787,37 +10724,6 @@ components:
           format: uri
         results:
           $ref: '#/components/schemas/ExtractedResults'
-    ProjectListFullSearchParams:
-      title: ProjectListFullSearchParams
-      type: object
-      properties:
-        limit:
-          title: Limit
-          exclusiveMaximum: true
-          minimum: 1
-          type: integer
-          description: maximum number of items to return (pagination)
-          default: 20
-          maximum: 50
-        offset:
-          title: Offset
-          minimum: 0
-          type: integer
-          description: index to the first item to return (pagination)
-          default: 0
-        text:
-          title: Text
-          maxLength: 100
-          type: string
-          description: Multi column full text search, across all folders and workspaces
-          example: My Project
-        tag_ids:
-          title: Tag Ids
-          type: string
-          description: Search by tag ID (multiple tag IDs may be provided separated
-            by column)
-          example: 1,3
-      description: Use as pagination options in query parameters
     ProjectListItem:
       title: ProjectListItem
       required:
diff --git a/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py b/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py
index 627d733d9c7..07be7223107 100644
--- a/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/api_keys/_handlers.py
@@ -3,17 +3,15 @@
 from aiohttp import web
 from aiohttp.web import RouteTableDef
 from models_library.api_schemas_webserver.auth import ApiKeyCreate
-from models_library.users import UserID
-from pydantic import Field
 from servicelib.aiohttp import status
-from servicelib.aiohttp.requests_validation import RequestParams, parse_request_body_as
+from servicelib.aiohttp.requests_validation import parse_request_body_as
 from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
 from simcore_postgres_database.errors import DatabaseError
 from simcore_service_webserver.security.decorators import permission_required
 
-from .._constants import RQ_PRODUCT_KEY, RQT_USERID_KEY
 from .._meta import API_VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..utils_aiohttp import envelope_json_response
 from . import _api
 
@@ -23,16 +21,11 @@
 routes = RouteTableDef()
 
 
-class _RequestContext(RequestParams):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
 @routes.get(f"/{API_VTAG}/auth/api-keys", name="list_api_keys")
 @login_required
 @permission_required("user.apikey.*")
 async def list_api_keys(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     api_keys_names = await _api.list_api_keys(
         request.app,
         user_id=req_ctx.user_id,
@@ -45,7 +38,7 @@ async def list_api_keys(request: web.Request):
 @login_required
 @permission_required("user.apikey.*")
 async def create_api_key(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     new = await parse_request_body_as(ApiKeyCreate, request)
     try:
         data = await _api.create_api_key(
@@ -67,7 +60,7 @@ async def create_api_key(request: web.Request):
 @login_required
 @permission_required("user.apikey.*")
 async def delete_api_key(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
 
     # NOTE: SEE https://github.com/ITISFoundation/osparc-simcore/issues/4920
     body = await request.json()
diff --git a/services/web/server/src/simcore_service_webserver/clusters/_handlers.py b/services/web/server/src/simcore_service_webserver/clusters/_handlers.py
index 70752da883b..1fe3f4975a0 100644
--- a/services/web/server/src/simcore_service_webserver/clusters/_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/clusters/_handlers.py
@@ -10,15 +10,13 @@
     ClusterPathParams,
     ClusterPing,
 )
-from models_library.users import UserID
-from pydantic import BaseModel, Field, parse_obj_as
+from pydantic import parse_obj_as
 from servicelib.aiohttp import status
 from servicelib.aiohttp.requests_validation import (
     parse_request_body_as,
     parse_request_path_parameters_as,
 )
 from servicelib.aiohttp.typing_extension import Handler
-from servicelib.request_keys import RQT_USERID_KEY
 
 from .._meta import api_version_prefix
 from ..director_v2 import api as director_v2_api
@@ -29,6 +27,7 @@
     DirectorServiceError,
 )
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..security.decorators import permission_required
 from ..utils_aiohttp import envelope_json_response
 
@@ -57,15 +56,6 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
     return wrapper
 
 
-#
-# API components/schemas
-#
-
-
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-
-
 #
 # API handlers
 #
@@ -78,7 +68,7 @@ class _RequestContext(BaseModel):
 @permission_required("clusters.create")
 @_handle_cluster_exceptions
 async def create_cluster(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     new_cluster = await parse_request_body_as(ClusterCreate, request)
 
     created_cluster = await director_v2_api.create_cluster(
@@ -94,7 +84,7 @@ async def create_cluster(request: web.Request) -> web.Response:
 @permission_required("clusters.read")
 @_handle_cluster_exceptions
 async def list_clusters(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
 
     clusters = await director_v2_api.list_clusters(
         app=request.app,
@@ -109,7 +99,7 @@ async def list_clusters(request: web.Request) -> web.Response:
 @permission_required("clusters.read")
 @_handle_cluster_exceptions
 async def get_cluster(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(ClusterPathParams, request)
 
     cluster = await director_v2_api.get_cluster(
@@ -126,7 +116,7 @@ async def get_cluster(request: web.Request) -> web.Response:
 @permission_required("clusters.write")
 @_handle_cluster_exceptions
 async def update_cluster(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(ClusterPathParams, request)
     cluster_patch = await parse_request_body_as(ClusterPatch, request)
 
@@ -146,7 +136,7 @@ async def update_cluster(request: web.Request) -> web.Response:
 @permission_required("clusters.delete")
 @_handle_cluster_exceptions
 async def delete_cluster(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(ClusterPathParams, request)
 
     await director_v2_api.delete_cluster(
@@ -165,7 +155,7 @@ async def delete_cluster(request: web.Request) -> web.Response:
 @permission_required("clusters.read")
 @_handle_cluster_exceptions
 async def get_cluster_details(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(ClusterPathParams, request)
 
     cluster_details = await director_v2_api.get_cluster_details(
@@ -199,7 +189,7 @@ async def ping_cluster(request: web.Request) -> web.Response:
 @permission_required("clusters.read")
 @_handle_cluster_exceptions
 async def ping_cluster_cluster_id(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(ClusterPathParams, request)
 
     await director_v2_api.ping_specific_cluster(
diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py b/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py
index f794fa6f148..111ca1f6298 100644
--- a/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/director_v2/_handlers.py
@@ -22,10 +22,10 @@
     GroupExtraPropertiesRepo,
 )
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import API_VTAG as VTAG
 from ..db.plugin import get_database_engine
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..products import api as products_api
 from ..security.decorators import permission_required
 from ..users.exceptions import UserDefaultWalletNotFoundError
@@ -43,11 +43,6 @@
 routes = web.RouteTableDef()
 
 
-class RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
 class _ComputationStarted(BaseModel):
     pipeline_id: ProjectID = Field(
         ..., description="ID for created pipeline (=project identifier)"
diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py b/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py
index b1a01ef61aa..e8a888cf541 100644
--- a/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/folders/_folders_handlers.py
@@ -28,8 +28,8 @@
 from ._exceptions_handlers import handle_plugin_requests_exceptions
 from ._models import (
     FolderFilters,
-    FolderListFullSearchWithJsonStrQueryParams,
-    FolderListWithJsonStrQueryParams,
+    FolderSearchQueryParams,
+    FoldersListQueryParams,
     FoldersPathParams,
     FoldersRequestContext,
 )
@@ -66,8 +66,8 @@ async def create_folder(request: web.Request):
 @handle_plugin_requests_exceptions
 async def list_folders(request: web.Request):
     req_ctx = FoldersRequestContext.parse_obj(request)
-    query_params: FolderListWithJsonStrQueryParams = parse_request_query_parameters_as(
-        FolderListWithJsonStrQueryParams, request
+    query_params: FoldersListQueryParams = parse_request_query_parameters_as(
+        FoldersListQueryParams, request
     )
 
     if not query_params.filters:
@@ -106,10 +106,8 @@ async def list_folders(request: web.Request):
 @handle_plugin_requests_exceptions
 async def list_folders_full_search(request: web.Request):
     req_ctx = FoldersRequestContext.parse_obj(request)
-    query_params: FolderListFullSearchWithJsonStrQueryParams = (
-        parse_request_query_parameters_as(
-            FolderListFullSearchWithJsonStrQueryParams, request
-        )
+    query_params: FolderSearchQueryParams = parse_request_query_parameters_as(
+        FolderSearchQueryParams, request
     )
 
     if not query_params.filters:
diff --git a/services/web/server/src/simcore_service_webserver/folders/_models.py b/services/web/server/src/simcore_service_webserver/folders/_models.py
index 899514a271b..766b34bf995 100644
--- a/services/web/server/src/simcore_service_webserver/folders/_models.py
+++ b/services/web/server/src/simcore_service_webserver/folders/_models.py
@@ -2,8 +2,13 @@
 
 from models_library.basic_types import IDStr
 from models_library.folders import FolderID
+from models_library.rest_base import RequestParameters, StrictRequestParameters
 from models_library.rest_filters import Filters, FiltersQueryParameters
-from models_library.rest_ordering import OrderBy, OrderDirection
+from models_library.rest_ordering import (
+    OrderBy,
+    OrderDirection,
+    create_ordering_query_model_classes,
+)
 from models_library.rest_pagination import PageQueryParameters
 from models_library.users import UserID
 from models_library.utils.common_validators import (
@@ -11,8 +16,7 @@
     null_or_none_str_to_none_validator,
 )
 from models_library.workspaces import WorkspaceID
-from pydantic import BaseModel, Extra, Field, Json, validator
-from servicelib.aiohttp.requests_validation import RequestParams, StrictRequestParams
+from pydantic import BaseModel, Extra, Field, validator
 from servicelib.request_keys import RQT_USERID_KEY
 
 from .._constants import RQ_PRODUCT_KEY
@@ -20,12 +24,12 @@
 _logger = logging.getLogger(__name__)
 
 
-class FoldersRequestContext(RequestParams):
+class FoldersRequestContext(RequestParameters):
     user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
     product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
 
 
-class FoldersPathParams(StrictRequestParams):
+class FoldersPathParams(StrictRequestParameters):
     folder_id: FolderID
 
 
@@ -36,35 +40,18 @@ class FolderFilters(Filters):
     )
 
 
-class FolderListSortParams(BaseModel):
-    # pylint: disable=unsubscriptable-object
-    order_by: Json[OrderBy] = Field(
-        default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
-        description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
-        example='{"field": "name", "direction": "desc"}',
-        alias="order_by",
-    )
-
-    @validator("order_by", check_fields=False)
-    @classmethod
-    def _validate_order_by_field(cls, v):
-        if v.field not in {
-            "modified_at",
-            "name",
-            "description",
-        }:
-            msg = f"We do not support ordering by provided field {v.field}"
-            raise ValueError(msg)
-        if v.field == "modified_at":
-            v.field = "modified"
-        return v
-
-    class Config:
-        extra = Extra.forbid
+_FolderOrderQueryParams: type[RequestParameters] = create_ordering_query_model_classes(
+    ordering_fields={
+        "modified_at",
+        "name",
+    },
+    default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
+    ordering_fields_api_to_column_map={"modified_at": "modified"},
+)
 
 
-class FolderListWithJsonStrQueryParams(
-    PageQueryParameters, FolderListSortParams, FiltersQueryParameters[FolderFilters]
+class FoldersListQueryParams(
+    PageQueryParameters, _FolderOrderQueryParams, FiltersQueryParameters[FolderFilters]  # type: ignore[misc, valid-type]
 ):
     folder_id: FolderID | None = Field(
         default=None,
@@ -88,8 +75,8 @@ class Config:
     )(null_or_none_str_to_none_validator)
 
 
-class FolderListFullSearchWithJsonStrQueryParams(
-    PageQueryParameters, FolderListSortParams, FiltersQueryParameters[FolderFilters]
+class FolderSearchQueryParams(
+    PageQueryParameters, _FolderOrderQueryParams, FiltersQueryParameters[FolderFilters]  # type: ignore[misc, valid-type]
 ):
     text: str | None = Field(
         default=None,
diff --git a/services/web/server/src/simcore_service_webserver/long_running_tasks.py b/services/web/server/src/simcore_service_webserver/long_running_tasks.py
index a7e4e8c725b..cd9fa77e07e 100644
--- a/services/web/server/src/simcore_service_webserver/long_running_tasks.py
+++ b/services/web/server/src/simcore_service_webserver/long_running_tasks.py
@@ -1,25 +1,16 @@
 from functools import wraps
 
 from aiohttp import web
-from models_library.users import UserID
 from models_library.utils.fastapi_encoders import jsonable_encoder
-from pydantic import Field
 from servicelib.aiohttp.long_running_tasks._constants import (
     RQT_LONG_RUNNING_TASKS_CONTEXT_KEY,
 )
 from servicelib.aiohttp.long_running_tasks.server import setup
-from servicelib.aiohttp.requests_validation import RequestParams
 from servicelib.aiohttp.typing_extension import Handler
-from servicelib.request_keys import RQT_USERID_KEY
 
-from ._constants import RQ_PRODUCT_KEY
 from ._meta import API_VTAG
 from .login.decorators import login_required
-
-
-class _RequestContext(RequestParams):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
+from .models import RequestContext
 
 
 def _webserver_request_context_decorator(handler: Handler):
@@ -28,7 +19,7 @@ async def _test_task_context_decorator(
         request: web.Request,
     ) -> web.StreamResponse:
         """this task context callback tries to get the user_id from the query if available"""
-        req_ctx = _RequestContext.parse_obj(request)
+        req_ctx = RequestContext.parse_obj(request)
         request[RQT_LONG_RUNNING_TASKS_CONTEXT_KEY] = jsonable_encoder(req_ctx)
         return await handler(request)
 
diff --git a/services/web/server/src/simcore_service_webserver/models.py b/services/web/server/src/simcore_service_webserver/models.py
new file mode 100644
index 00000000000..48ffd369586
--- /dev/null
+++ b/services/web/server/src/simcore_service_webserver/models.py
@@ -0,0 +1,11 @@
+from models_library.rest_base import RequestParameters
+from models_library.users import UserID
+from pydantic import Field
+from servicelib.request_keys import RQT_USERID_KEY
+
+from ._constants import RQ_PRODUCT_KEY
+
+
+class RequestContext(RequestParameters):
+    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
+    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
diff --git a/services/web/server/src/simcore_service_webserver/products/_handlers.py b/services/web/server/src/simcore_service_webserver/products/_handlers.py
index bfdabef6d6f..1d7e4e4bc57 100644
--- a/services/web/server/src/simcore_service_webserver/products/_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/products/_handlers.py
@@ -4,13 +4,10 @@
 from aiohttp import web
 from models_library.api_schemas_webserver.product import GetCreditPrice, GetProduct
 from models_library.basic_types import IDStr
+from models_library.rest_base import RequestParameters, StrictRequestParameters
 from models_library.users import UserID
 from pydantic import Extra, Field
-from servicelib.aiohttp.requests_validation import (
-    RequestParams,
-    StrictRequestParams,
-    parse_request_path_parameters_as,
-)
+from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as
 from servicelib.request_keys import RQT_USERID_KEY
 from simcore_service_webserver.utils_aiohttp import envelope_json_response
 
@@ -27,7 +24,7 @@
 _logger = logging.getLogger(__name__)
 
 
-class _ProductsRequestContext(RequestParams):
+class _ProductsRequestContext(RequestParameters):
     user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
     product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
 
@@ -49,7 +46,7 @@ async def _get_current_product_price(request: web.Request):
     return envelope_json_response(credit_price)
 
 
-class _ProductsRequestParams(StrictRequestParams):
+class _ProductsRequestParams(StrictRequestParameters):
     product_name: IDStr | Literal["current"]
 
 
diff --git a/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py b/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py
index a300a4c43e9..905be090f47 100644
--- a/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/products/_invitations_handlers.py
@@ -6,9 +6,10 @@
     GenerateInvitation,
     InvitationGenerated,
 )
+from models_library.rest_base import RequestParameters
 from models_library.users import UserID
 from pydantic import Field
-from servicelib.aiohttp.requests_validation import RequestParams, parse_request_body_as
+from servicelib.aiohttp.requests_validation import parse_request_body_as
 from servicelib.request_keys import RQT_USERID_KEY
 from simcore_service_webserver.utils_aiohttp import envelope_json_response
 from yarl import URL
@@ -26,7 +27,7 @@
 _logger = logging.getLogger(__name__)
 
 
-class _ProductsRequestContext(RequestParams):
+class _ProductsRequestContext(RequestParameters):
     user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
     product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
 
diff --git a/services/web/server/src/simcore_service_webserver/projects/_common_models.py b/services/web/server/src/simcore_service_webserver/projects/_common_models.py
index 073c012a8ac..a39aaef626f 100644
--- a/services/web/server/src/simcore_service_webserver/projects/_common_models.py
+++ b/services/web/server/src/simcore_service_webserver/projects/_common_models.py
@@ -5,16 +5,11 @@
 """
 
 from models_library.projects import ProjectID
-from models_library.users import UserID
 from pydantic import BaseModel, Extra, Field
-from servicelib.request_keys import RQT_USERID_KEY
 
-from .._constants import RQ_PRODUCT_KEY
+from ..models import RequestContext
 
-
-class RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
+assert RequestContext.__name__  # nosec
 
 
 class ProjectPathParams(BaseModel):
@@ -29,3 +24,6 @@ class RemoveQueryParams(BaseModel):
     force: bool = Field(
         default=False, description="Force removal (even if resource is active)"
     )
+
+
+__all__: tuple[str, ...] = ("RequestContext",)
diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py
index 7500a6a4d26..cdbbe479182 100644
--- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py
@@ -51,12 +51,12 @@
 from . import _crud_api_create, _crud_api_read, projects_api
 from ._common_models import ProjectPathParams, RequestContext
 from ._crud_handlers_models import (
-    ProjectActiveParams,
+    ProjectActiveQueryParams,
     ProjectCreateHeaders,
     ProjectCreateParams,
     ProjectFilters,
-    ProjectListFullSearchWithJsonStrParams,
-    ProjectListWithJsonStrParams,
+    ProjectsListQueryParams,
+    ProjectsSearchQueryParams,
 )
 from ._permalink_api import update_or_pop_permalink_in_project
 from .exceptions import (
@@ -188,8 +188,8 @@ async def list_projects(request: web.Request):
 
     """
     req_ctx = RequestContext.parse_obj(request)
-    query_params: ProjectListWithJsonStrParams = parse_request_query_parameters_as(
-        ProjectListWithJsonStrParams, request
+    query_params: ProjectsListQueryParams = parse_request_query_parameters_as(
+        ProjectsListQueryParams, request
     )
 
     if not query_params.filters:
@@ -233,10 +233,8 @@ async def list_projects(request: web.Request):
 @_handle_projects_exceptions
 async def list_projects_full_search(request: web.Request):
     req_ctx = RequestContext.parse_obj(request)
-    query_params: ProjectListFullSearchWithJsonStrParams = (
-        parse_request_query_parameters_as(
-            ProjectListFullSearchWithJsonStrParams, request
-        )
+    query_params: ProjectsSearchQueryParams = parse_request_query_parameters_as(
+        ProjectsSearchQueryParams, request
     )
     tag_ids_list = query_params.tag_ids_list()
 
@@ -283,8 +281,8 @@ async def get_active_project(request: web.Request) -> web.Response:
         web.HTTPNotFound: If active project is not found
     """
     req_ctx = RequestContext.parse_obj(request)
-    query_params: ProjectActiveParams = parse_request_query_parameters_as(
-        ProjectActiveParams, request
+    query_params: ProjectActiveQueryParams = parse_request_query_parameters_as(
+        ProjectActiveQueryParams, request
     )
 
     try:
diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py
index b1c499fd3a9..43800a164e3 100644
--- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py
+++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers_models.py
@@ -10,23 +10,20 @@
 from models_library.folders import FolderID
 from models_library.projects import ProjectID
 from models_library.projects_nodes_io import NodeID
+from models_library.rest_base import RequestParameters
 from models_library.rest_filters import Filters, FiltersQueryParameters
-from models_library.rest_ordering import OrderBy, OrderDirection
+from models_library.rest_ordering import (
+    OrderBy,
+    OrderDirection,
+    create_ordering_query_model_classes,
+)
 from models_library.rest_pagination import PageQueryParameters
 from models_library.utils.common_validators import (
     empty_str_to_none_pre_validator,
     null_or_none_str_to_none_validator,
 )
 from models_library.workspaces import WorkspaceID
-from pydantic import (
-    BaseModel,
-    Extra,
-    Field,
-    Json,
-    parse_obj_as,
-    root_validator,
-    validator,
-)
+from pydantic import BaseModel, Extra, Field, parse_obj_as, root_validator, validator
 from servicelib.common_headers import (
     UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE,
     X_SIMCORE_PARENT_NODE_ID,
@@ -104,7 +101,21 @@ class ProjectFilters(Filters):
     )
 
 
-class ProjectListParams(PageQueryParameters):
+ProjectsListOrderParams = create_ordering_query_model_classes(
+    ordering_fields={
+        "type",
+        "uuid",
+        "name",
+        "description",
+        "prj_owner",
+        "creation_date",
+        "last_change_date",
+    },
+    default=OrderBy(field=IDStr("last_change_date"), direction=OrderDirection.DESC),
+)
+
+
+class ProjectsListExtraQueryParams(RequestParameters):
     project_type: ProjectTypeAPI = Field(default=ProjectTypeAPI.all, alias="type")
     show_hidden: bool = Field(
         default=False, description="includes projects marked as hidden in the listing"
@@ -140,45 +151,20 @@ def search_check_empty_string(cls, v):
     )(null_or_none_str_to_none_validator)
 
 
-class ProjectListSortParams(BaseModel):
-    order_by: Json[OrderBy] = Field(  # pylint: disable=unsubscriptable-object
-        default=OrderBy(field=IDStr("last_change_date"), direction=OrderDirection.DESC),
-        description="Order by field (type|uuid|name|description|prj_owner|creation_date|last_change_date) and direction (asc|desc). The default sorting order is ascending.",
-        example='{"field": "prj_owner", "direction": "desc"}',
-        alias="order_by",
-    )
-
-    @validator("order_by", check_fields=False)
-    @classmethod
-    def validate_order_by_field(cls, v):
-        if v.field not in {
-            "type",
-            "uuid",
-            "name",
-            "description",
-            "prj_owner",
-            "creation_date",
-            "last_change_date",
-        }:
-            msg = f"We do not support ordering by provided field {v.field}"
-            raise ValueError(msg)
-        return v
-
-    class Config:
-        extra = Extra.forbid
-
-
-class ProjectListWithJsonStrParams(
-    ProjectListParams, ProjectListSortParams, FiltersQueryParameters[ProjectFilters]
+class ProjectsListQueryParams(
+    PageQueryParameters,
+    ProjectsListOrderParams,  # type: ignore[misc, valid-type]
+    FiltersQueryParameters[ProjectFilters],
+    ProjectsListExtraQueryParams,
 ):
     ...
 
 
-class ProjectActiveParams(BaseModel):
+class ProjectActiveQueryParams(BaseModel):
     client_session_id: str
 
 
-class ProjectListFullSearchParams(PageQueryParameters):
+class ProjectSearchExtraQueryParams(PageQueryParameters):
     text: str | None = Field(
         default=None,
         description="Multi column full text search, across all folders and workspaces",
@@ -196,8 +182,8 @@ class ProjectListFullSearchParams(PageQueryParameters):
     )
 
 
-class ProjectListFullSearchWithJsonStrParams(
-    ProjectListFullSearchParams, ProjectListSortParams
+class ProjectsSearchQueryParams(
+    ProjectSearchExtraQueryParams, ProjectsListOrderParams  # type: ignore[misc, valid-type]
 ):
     def tag_ids_list(self) -> list[int]:
         try:
diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py
index d8b4749a37a..a0f7f60f0e8 100644
--- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_handlers.py
@@ -19,19 +19,18 @@
     PricingUnitWithCostCreate,
     PricingUnitWithCostUpdate,
 )
-from models_library.users import UserID
-from pydantic import BaseModel, Extra, Field
+from models_library.rest_base import StrictRequestParameters
+from pydantic import BaseModel, Extra
 from servicelib.aiohttp.requests_validation import (
     parse_request_body_as,
     parse_request_path_parameters_as,
 )
 from servicelib.aiohttp.typing_extension import Handler
 from servicelib.rabbitmq._errors import RPCServerError
-from servicelib.request_keys import RQT_USERID_KEY
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import API_VTAG as VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..security.decorators import permission_required
 from ..utils_aiohttp import envelope_json_response
 from . import _pricing_plans_admin_api as admin_api
@@ -55,11 +54,6 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
     return wrapper
 
 
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
 #
 # API handlers
 #
@@ -70,7 +64,7 @@ class _RequestContext(BaseModel):
 ## Admin Pricing Plan endpoints
 
 
-class _GetPricingPlanPathParams(BaseModel):
+class PricingPlanGetPathParams(StrictRequestParameters):
     pricing_plan_id: PricingPlanId
 
     class Config:
@@ -85,7 +79,7 @@ class Config:
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def list_pricing_plans(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
 
     pricing_plans_list = await admin_api.list_pricing_plans(
         app=request.app,
@@ -116,8 +110,8 @@ async def list_pricing_plans(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def get_pricing_plan(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingPlanGetPathParams, request)
 
     pricing_plan_get = await admin_api.get_pricing_plan(
         app=request.app,
@@ -125,7 +119,8 @@ async def get_pricing_plan(request: web.Request):
         pricing_plan_id=path_params.pricing_plan_id,
     )
     if pricing_plan_get.pricing_units is None:
-        raise ValueError("Pricing plan units should not be None")
+        msg = "Pricing plan units should not be None"
+        raise ValueError(msg)
 
     webserver_admin_pricing_plan_get = PricingPlanAdminGet(
         pricing_plan_id=pricing_plan_get.pricing_plan_id,
@@ -159,7 +154,7 @@ async def get_pricing_plan(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def create_pricing_plan(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     body_params = await parse_request_body_as(CreatePricingPlanBodyParams, request)
 
     _data = PricingPlanCreate(
@@ -208,8 +203,8 @@ async def create_pricing_plan(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def update_pricing_plan(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingPlanGetPathParams, request)
     body_params = await parse_request_body_as(UpdatePricingPlanBodyParams, request)
 
     _data = PricingPlanUpdate(
@@ -253,7 +248,7 @@ async def update_pricing_plan(request: web.Request):
 ## Admin Pricing Unit endpoints
 
 
-class _GetPricingUnitPathParams(BaseModel):
+class PricingUnitGetPathParams(BaseModel):
     pricing_plan_id: PricingPlanId
     pricing_unit_id: PricingUnitId
 
@@ -269,8 +264,8 @@ class Config:
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def get_pricing_unit(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingUnitPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingUnitGetPathParams, request)
 
     pricing_unit_get = await admin_api.get_pricing_unit(
         app=request.app,
@@ -299,8 +294,8 @@ async def get_pricing_unit(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def create_pricing_unit(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingPlanGetPathParams, request)
     body_params = await parse_request_body_as(CreatePricingUnitBodyParams, request)
 
     _data = PricingUnitWithCostCreate(
@@ -338,8 +333,8 @@ async def create_pricing_unit(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def update_pricing_unit(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingUnitPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingUnitGetPathParams, request)
     body_params = await parse_request_body_as(UpdatePricingUnitBodyParams, request)
 
     _data = PricingUnitWithCostUpdate(
@@ -380,8 +375,8 @@ async def update_pricing_unit(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def list_connected_services_to_pricing_plan(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingPlanGetPathParams, request)
 
     connected_services_list = await admin_api.list_connected_services_to_pricing_plan(
         app=request.app,
@@ -409,8 +404,8 @@ async def list_connected_services_to_pricing_plan(request: web.Request):
 @permission_required("resource-usage.write")
 @_handle_pricing_plan_admin_exceptions
 async def connect_service_to_pricing_plan(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    path_params = parse_request_path_parameters_as(_GetPricingPlanPathParams, request)
+    req_ctx = RequestContext.parse_obj(request)
+    path_params = parse_request_path_parameters_as(PricingPlanGetPathParams, request)
     body_params = await parse_request_body_as(
         ConnectServiceToPricingPlanBodyParams, request
     )
diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py
index 86072f00e5e..dc2949113a6 100644
--- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_handlers.py
@@ -3,15 +3,13 @@
 from aiohttp import web
 from models_library.api_schemas_webserver.resource_usage import PricingUnitGet
 from models_library.resource_tracker import PricingPlanId, PricingUnitId
-from models_library.users import UserID
-from pydantic import BaseModel, Extra, Field
+from models_library.rest_base import StrictRequestParameters
 from servicelib.aiohttp.requests_validation import parse_request_path_parameters_as
 from servicelib.aiohttp.typing_extension import Handler
-from servicelib.request_keys import RQT_USERID_KEY
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import API_VTAG as VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..security.decorators import permission_required
 from ..utils_aiohttp import envelope_json_response
 from ..wallets.errors import WalletAccessForbiddenError
@@ -34,25 +32,13 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
     return wrapper
 
 
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
-#
-# API handlers
-#
-
 routes = web.RouteTableDef()
 
 
-class _GetPricingPlanUnitPathParams(BaseModel):
+class PricingPlanUnitGetPathParams(StrictRequestParameters):
     pricing_plan_id: PricingPlanId
     pricing_unit_id: PricingUnitId
 
-    class Config:
-        extra = Extra.forbid
-
 
 @routes.get(
     f"/{VTAG}/pricing-plans/{{pricing_plan_id}}/pricing-units/{{pricing_unit_id}}",
@@ -62,9 +48,9 @@ class Config:
 @permission_required("resource-usage.read")
 @_handle_resource_usage_exceptions
 async def get_pricing_plan_unit(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(
-        _GetPricingPlanUnitPathParams, request
+        PricingPlanUnitGetPathParams, request
     )
 
     pricing_unit_get = await api.get_pricing_plan_unit(
diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py
index f265e45faf1..cf98bff12a7 100644
--- a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_handlers.py
@@ -12,34 +12,24 @@
     ServicesAggregatedUsagesTimePeriod,
     ServicesAggregatedUsagesType,
 )
-from models_library.rest_ordering import OrderBy, OrderDirection
-from models_library.rest_pagination import (
-    DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
-    MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE,
-    Page,
-    PageQueryParameters,
+from models_library.rest_base import RequestParameters
+from models_library.rest_ordering import (
+    OrderBy,
+    OrderDirection,
+    create_ordering_query_model_classes,
 )
+from models_library.rest_pagination import Page, PageQueryParameters
 from models_library.rest_pagination_utils import paginate_data
-from models_library.users import UserID
 from models_library.wallets import WalletID
-from pydantic import (
-    BaseModel,
-    Extra,
-    Field,
-    Json,
-    NonNegativeInt,
-    parse_obj_as,
-    validator,
-)
+from pydantic import Extra, Field, Json, parse_obj_as
 from servicelib.aiohttp.requests_validation import parse_request_query_parameters_as
 from servicelib.aiohttp.typing_extension import Handler
 from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON
-from servicelib.request_keys import RQT_USERID_KEY
 from servicelib.rest_constants import RESPONSE_MODEL_POLICY
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import API_VTAG as VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..security.decorators import permission_required
 from ..wallets.errors import WalletAccessForbiddenError
 from . import _service_runs_api as api
@@ -61,21 +51,40 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
     return wrapper
 
 
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
-ORDER_BY_DESCRIPTION = "Order by field (wallet_id|wallet_name|user_id|project_id|project_name|node_id|node_name|service_key|service_version|service_type|started_at|stopped_at|service_run_status|credit_cost|transaction_status) and direction (asc|desc). The default sorting order is ascending."
+_ResorceUsagesListOrderQueryParams: type[
+    RequestParameters
+] = create_ordering_query_model_classes(
+    ordering_fields={
+        "wallet_id",
+        "wallet_name",
+        "user_id",
+        "user_email",
+        "project_id",
+        "project_name",
+        "node_id",
+        "node_name",
+        "root_parent_project_id",
+        "root_parent_project_name",
+        "service_key",
+        "service_version",
+        "service_type",
+        "started_at",
+        "stopped_at",
+        "service_run_status",
+        "credit_cost",
+        "transaction_status",
+    },
+    default=OrderBy(field=IDStr("started_at"), direction=OrderDirection.DESC),
+    ordering_fields_api_to_column_map={
+        "credit_cost": "osparc_credits",
+    },
+)
 
 
-class _ListServicesResourceUsagesQueryParams(BaseModel):
+class ServicesResourceUsagesReportQueryParams(
+    _ResorceUsagesListOrderQueryParams  # type: ignore[misc, valid-type]
+):
     wallet_id: WalletID | None = Field(default=None)
-    order_by: Json[OrderBy] = Field(  # pylint: disable=unsubscriptable-object
-        default=OrderBy(field=IDStr("started_at"), direction=OrderDirection.DESC),
-        description=ORDER_BY_DESCRIPTION,
-        example='{"field": "started_at", "direction": "desc"}',
-    )
     filters: (
         Json[ServiceResourceUsagesFilters]  # pylint: disable=unsubscriptable-object
         | None
@@ -85,56 +94,18 @@ class _ListServicesResourceUsagesQueryParams(BaseModel):
         example='{"started_at": {"from": "yyyy-mm-dd", "until": "yyyy-mm-dd"}}',
     )
 
-    @validator("order_by", allow_reuse=True)
-    @classmethod
-    def validate_order_by_field(cls, v):
-        if v.field not in {
-            "wallet_id",
-            "wallet_name",
-            "user_id",
-            "user_email",
-            "project_id",
-            "project_name",
-            "node_id",
-            "node_name",
-            "root_parent_project_id",
-            "root_parent_project_name",
-            "service_key",
-            "service_version",
-            "service_type",
-            "started_at",
-            "stopped_at",
-            "service_run_status",
-            "credit_cost",
-            "transaction_status",
-        }:
-            raise ValueError(f"We do not support ordering by provided field {v.field}")
-        if v.field == "credit_cost":
-            v.field = "osparc_credits"
-        return v
-
     class Config:
         extra = Extra.forbid
 
 
-class _ListServicesResourceUsagesQueryParamsWithPagination(
-    _ListServicesResourceUsagesQueryParams
+class ServicesResourceUsagesListQueryParams(
+    PageQueryParameters, ServicesResourceUsagesReportQueryParams
 ):
-    limit: int = Field(
-        default=DEFAULT_NUMBER_OF_ITEMS_PER_PAGE,
-        description="maximum number of items to return (pagination)",
-        ge=1,
-        lt=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE,
-    )
-    offset: NonNegativeInt = Field(
-        default=0, description="index to the first item to return (pagination)"
-    )
-
     class Config:
         extra = Extra.forbid
 
 
-class _ListServicesAggregatedUsagesQueryParams(PageQueryParameters):
+class ServicesAggregatedUsagesListQueryParams(PageQueryParameters):
     aggregated_by: ServicesAggregatedUsagesType
     time_period: ServicesAggregatedUsagesTimePeriod
     wallet_id: WalletID
@@ -155,10 +126,10 @@ class Config:
 @permission_required("resource-usage.read")
 @_handle_resource_usage_exceptions
 async def list_resource_usage_services(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    query_params: _ListServicesResourceUsagesQueryParamsWithPagination = (
+    req_ctx = RequestContext.parse_obj(request)
+    query_params: ServicesResourceUsagesListQueryParams = (
         parse_request_query_parameters_as(
-            _ListServicesResourceUsagesQueryParamsWithPagination, request
+            ServicesResourceUsagesListQueryParams, request
         )
     )
 
@@ -196,10 +167,10 @@ async def list_resource_usage_services(request: web.Request):
 @permission_required("resource-usage.read")
 @_handle_resource_usage_exceptions
 async def list_osparc_credits_aggregated_usages(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    query_params: _ListServicesAggregatedUsagesQueryParams = (
+    req_ctx = RequestContext.parse_obj(request)
+    query_params: ServicesAggregatedUsagesListQueryParams = (
         parse_request_query_parameters_as(
-            _ListServicesAggregatedUsagesQueryParams, request
+            ServicesAggregatedUsagesListQueryParams, request
         )
     )
 
@@ -236,10 +207,10 @@ async def list_osparc_credits_aggregated_usages(request: web.Request):
 @permission_required("resource-usage.read")
 @_handle_resource_usage_exceptions
 async def export_resource_usage_services(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
-    query_params: _ListServicesResourceUsagesQueryParams = (
+    req_ctx = RequestContext.parse_obj(request)
+    query_params: ServicesResourceUsagesReportQueryParams = (
         parse_request_query_parameters_as(
-            _ListServicesResourceUsagesQueryParams, request
+            ServicesResourceUsagesReportQueryParams, request
         )
     )
     download_url = await api.export_usage_services(
diff --git a/services/web/server/src/simcore_service_webserver/tags/schemas.py b/services/web/server/src/simcore_service_webserver/tags/schemas.py
index 01663e0d337..c9d4a9d90a1 100644
--- a/services/web/server/src/simcore_service_webserver/tags/schemas.py
+++ b/services/web/server/src/simcore_service_webserver/tags/schemas.py
@@ -2,18 +2,18 @@
 from datetime import datetime
 
 from models_library.api_schemas_webserver._base import InputSchema, OutputSchema
+from models_library.rest_base import RequestParameters, StrictRequestParameters
 from models_library.users import GroupID, UserID
 from pydantic import ConstrainedStr, Field, PositiveInt
-from servicelib.aiohttp.requests_validation import RequestParams, StrictRequestParams
 from servicelib.request_keys import RQT_USERID_KEY
 from simcore_postgres_database.utils_tags import TagDict
 
 
-class TagRequestContext(RequestParams):
+class TagRequestContext(RequestParameters):
     user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
 
 
-class TagPathParams(StrictRequestParams):
+class TagPathParams(StrictRequestParameters):
     tag_id: PositiveInt
 
 
diff --git a/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py b/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py
index 0c537278f9c..3717fd0dd83 100644
--- a/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/users/_preferences_handlers.py
@@ -5,34 +5,25 @@
     PatchPathParams,
     PatchRequestBody,
 )
-from models_library.products import ProductName
-from models_library.users import UserID
-from pydantic import BaseModel, Field
 from servicelib.aiohttp import status
 from servicelib.aiohttp.requests_validation import (
     parse_request_body_as,
     parse_request_path_parameters_as,
 )
 from servicelib.aiohttp.typing_extension import Handler
-from servicelib.request_keys import RQT_USERID_KEY
 from simcore_postgres_database.utils_user_preferences import (
     CouldNotCreateOrUpdateUserPreferenceError,
 )
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import API_VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from . import _preferences_api
 from .exceptions import FrontendUserPreferenceIsNotDefinedError
 
 routes = web.RouteTableDef()
 
 
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: ProductName = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
 def _handle_users_exceptions(handler: Handler):
     @functools.wraps(handler)
     async def wrapper(request: web.Request) -> web.StreamResponse:
@@ -55,7 +46,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
 @login_required
 @_handle_users_exceptions
 async def set_frontend_preference(request: web.Request) -> web.Response:
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     req_body = await parse_request_body_as(PatchRequestBody, request)
     req_path_params = parse_request_path_parameters_as(PatchPathParams, request)
 
diff --git a/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py
index 1115a239d62..0f0e2552986 100644
--- a/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py
@@ -6,20 +6,19 @@
 import logging
 
 from aiohttp import web
-from models_library.users import GroupID, UserID
+from models_library.users import GroupID
 from models_library.wallets import WalletID
-from pydantic import BaseModel, Extra, Field
+from pydantic import BaseModel, Extra
 from servicelib.aiohttp import status
 from servicelib.aiohttp.requests_validation import (
     parse_request_body_as,
     parse_request_path_parameters_as,
 )
 from servicelib.aiohttp.typing_extension import Handler
-from servicelib.request_keys import RQT_USERID_KEY
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import api_version_prefix as VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..security.decorators import permission_required
 from ..utils_aiohttp import envelope_json_response
 from . import _groups_api
@@ -30,11 +29,6 @@
 _logger = logging.getLogger(__name__)
 
 
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
 def _handle_wallets_groups_exceptions(handler: Handler):
     @functools.wraps(handler)
     async def wrapper(request: web.Request) -> web.StreamResponse:
@@ -81,7 +75,7 @@ class Config:
 @permission_required("wallets.*")
 @_handle_wallets_groups_exceptions
 async def create_wallet_group(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(_WalletsGroupsPathParams, request)
     body_params = await parse_request_body_as(_WalletsGroupsBodyParams, request)
 
@@ -104,7 +98,7 @@ async def create_wallet_group(request: web.Request):
 @permission_required("wallets.*")
 @_handle_wallets_groups_exceptions
 async def list_wallet_groups(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(WalletsPathParams, request)
 
     wallets: list[
@@ -127,7 +121,7 @@ async def list_wallet_groups(request: web.Request):
 @permission_required("wallets.*")
 @_handle_wallets_groups_exceptions
 async def update_wallet_group(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(_WalletsGroupsPathParams, request)
     body_params = await parse_request_body_as(_WalletsGroupsBodyParams, request)
 
@@ -151,7 +145,7 @@ async def update_wallet_group(request: web.Request):
 @permission_required("wallets.*")
 @_handle_wallets_groups_exceptions
 async def delete_wallet_group(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(_WalletsGroupsPathParams, request)
 
     await _groups_api.delete_wallet_group(
diff --git a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py
index dc6855f2c01..954ed6b263b 100644
--- a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py
@@ -9,12 +9,11 @@
     WalletGetWithAvailableCredits,
 )
 from models_library.error_codes import create_error_code
+from models_library.rest_base import RequestParameters, StrictRequestParameters
 from models_library.users import UserID
 from models_library.wallets import WalletID
 from pydantic import Field
 from servicelib.aiohttp.requests_validation import (
-    RequestParams,
-    StrictRequestParams,
     parse_request_body_as,
     parse_request_path_parameters_as,
 )
@@ -106,19 +105,18 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
     return wrapper
 
 
-#
 # wallets COLLECTION -------------------------
 #
 
 routes = web.RouteTableDef()
 
 
-class WalletsRequestContext(RequestParams):
+class WalletsRequestContext(RequestParameters):
     user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
     product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
 
 
-class WalletsPathParams(StrictRequestParams):
+class WalletsPathParams(StrictRequestParameters):
     wallet_id: WalletID
 
 
diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py
index d4ae7c4b74f..c75ab891ef6 100644
--- a/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/workspaces/_groups_handlers.py
@@ -6,20 +6,19 @@
 import logging
 
 from aiohttp import web
-from models_library.users import GroupID, UserID
+from models_library.users import GroupID
 from models_library.workspaces import WorkspaceID
-from pydantic import BaseModel, Extra, Field
+from pydantic import BaseModel, Extra
 from servicelib.aiohttp import status
 from servicelib.aiohttp.requests_validation import (
     parse_request_body_as,
     parse_request_path_parameters_as,
 )
 from servicelib.aiohttp.typing_extension import Handler
-from servicelib.request_keys import RQT_USERID_KEY
 
-from .._constants import RQ_PRODUCT_KEY
 from .._meta import api_version_prefix as VTAG
 from ..login.decorators import login_required
+from ..models import RequestContext
 from ..security.decorators import permission_required
 from ..utils_aiohttp import envelope_json_response
 from . import _groups_api
@@ -30,11 +29,6 @@
 _logger = logging.getLogger(__name__)
 
 
-class _RequestContext(BaseModel):
-    user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
-    product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
-
-
 def _handle_workspaces_groups_exceptions(handler: Handler):
     @functools.wraps(handler)
     async def wrapper(request: web.Request) -> web.StreamResponse:
@@ -82,7 +76,7 @@ class Config:
 @permission_required("workspaces.*")
 @_handle_workspaces_groups_exceptions
 async def create_workspace_group(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request)
     body_params = await parse_request_body_as(_WorkspacesGroupsBodyParams, request)
 
@@ -105,7 +99,7 @@ async def create_workspace_group(request: web.Request):
 @permission_required("workspaces.*")
 @_handle_workspaces_groups_exceptions
 async def list_workspace_groups(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(WorkspacesPathParams, request)
 
     workspaces: list[
@@ -128,7 +122,7 @@ async def list_workspace_groups(request: web.Request):
 @permission_required("workspaces.*")
 @_handle_workspaces_groups_exceptions
 async def replace_workspace_group(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request)
     body_params = await parse_request_body_as(_WorkspacesGroupsBodyParams, request)
 
@@ -152,7 +146,7 @@ async def replace_workspace_group(request: web.Request):
 @permission_required("workspaces.*")
 @_handle_workspaces_groups_exceptions
 async def delete_workspace_group(request: web.Request):
-    req_ctx = _RequestContext.parse_obj(request)
+    req_ctx = RequestContext.parse_obj(request)
     path_params = parse_request_path_parameters_as(_WorkspacesGroupsPathParams, request)
 
     await _groups_api.delete_workspace_group(
diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py
index fa9a2e4aa67..f4e4b6b8088 100644
--- a/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py
+++ b/services/web/server/src/simcore_service_webserver/workspaces/_workspaces_handlers.py
@@ -9,16 +9,19 @@
     WorkspaceGetPage,
 )
 from models_library.basic_types import IDStr
-from models_library.rest_ordering import OrderBy, OrderDirection
+from models_library.rest_base import RequestParameters, StrictRequestParameters
+from models_library.rest_ordering import (
+    OrderBy,
+    OrderDirection,
+    create_ordering_query_model_classes,
+)
 from models_library.rest_pagination import Page, PageQueryParameters
 from models_library.rest_pagination_utils import paginate_data
 from models_library.users import UserID
 from models_library.workspaces import WorkspaceID
-from pydantic import Extra, Field, Json, parse_obj_as, validator
+from pydantic import Field, parse_obj_as
 from servicelib.aiohttp import status
 from servicelib.aiohttp.requests_validation import (
-    RequestParams,
-    StrictRequestParams,
     parse_request_body_as,
     parse_request_path_parameters_as,
     parse_request_query_parameters_as,
@@ -61,40 +64,32 @@ async def wrapper(request: web.Request) -> web.StreamResponse:
 routes = web.RouteTableDef()
 
 
-class WorkspacesRequestContext(RequestParams):
+class WorkspacesRequestContext(RequestParameters):
     user_id: UserID = Field(..., alias=RQT_USERID_KEY)  # type: ignore[literal-required]
     product_name: str = Field(..., alias=RQ_PRODUCT_KEY)  # type: ignore[literal-required]
 
 
-class WorkspacesPathParams(StrictRequestParams):
+class WorkspacesPathParams(StrictRequestParameters):
     workspace_id: WorkspaceID
 
 
-class WorkspacesListWithJsonStrQueryParams(PageQueryParameters):
-    # pylint: disable=unsubscriptable-object
-    order_by: Json[OrderBy] = Field(
-        default=OrderBy(field=IDStr("modified"), direction=OrderDirection.DESC),
-        description="Order by field (modified_at|name|description) and direction (asc|desc). The default sorting order is ascending.",
-        example='{"field": "name", "direction": "desc"}',
-        alias="order_by",
-    )
+WorkspacesListOrderQueryParams: type[
+    RequestParameters
+] = create_ordering_query_model_classes(
+    ordering_fields={
+        "modified_at",
+        "name",
+    },
+    default=OrderBy(field=IDStr("modified_at"), direction=OrderDirection.DESC),
+    ordering_fields_api_to_column_map={"modified_at": "modified"},
+)
 
-    @validator("order_by", check_fields=False)
-    @classmethod
-    def validate_order_by_field(cls, v):
-        if v.field not in {
-            "modified_at",
-            "name",
-            "description",
-        }:
-            msg = f"We do not support ordering by provided field {v.field}"
-            raise ValueError(msg)
-        if v.field == "modified_at":
-            v.field = "modified"
-        return v
 
-    class Config:
-        extra = Extra.forbid
+class WorkspacesListQueryParams(
+    PageQueryParameters,
+    WorkspacesListOrderQueryParams,  # type: ignore[misc, valid-type]
+):
+    ...
 
 
 @routes.post(f"/{VTAG}/workspaces", name="create_workspace")
@@ -123,8 +118,8 @@ async def create_workspace(request: web.Request):
 @handle_workspaces_exceptions
 async def list_workspaces(request: web.Request):
     req_ctx = WorkspacesRequestContext.parse_obj(request)
-    query_params: WorkspacesListWithJsonStrQueryParams = (
-        parse_request_query_parameters_as(WorkspacesListWithJsonStrQueryParams, request)
+    query_params: WorkspacesListQueryParams = parse_request_query_parameters_as(
+        WorkspacesListQueryParams, request
     )
 
     workspaces: WorkspaceGetPage = await _workspaces_api.list_workspaces(
diff --git a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py
index 9c8a29f2b6c..3af86589cfe 100644
--- a/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py
+++ b/services/web/server/tests/unit/with_dbs/03/resource_usage/test_usage_services__list.py
@@ -26,8 +26,8 @@
 
 _SERVICE_RUN_GET = ServiceRunPage(
     items=[
-        ServiceRunGet(
-            **{
+        ServiceRunGet.parse_obj(
+            {
                 "service_run_id": "comp_1_5c2110be-441b-11ee-a0e8-02420a000040_1",
                 "wallet_id": 1,
                 "wallet_name": "the super wallet!",
@@ -55,12 +55,11 @@
 
 @pytest.fixture
 def mock_list_usage_services(mocker: MockerFixture) -> tuple:
-    mock_list_usage = mocker.patch(
+    return mocker.patch(
         "simcore_service_webserver.resource_usage._service_runs_api.service_runs.get_service_run_page",
         spec=True,
         return_value=_SERVICE_RUN_GET,
     )
-    return mock_list_usage
 
 
 @pytest.fixture()
@@ -79,7 +78,10 @@ def setup_wallets_db(
             .returning(sa.literal_column("*"))
         )
         row = result.fetchone()
+        assert row
+
         yield cast(int, row[0])
+
         con.execute(wallets.delete())
 
 
@@ -160,6 +162,8 @@ async def test_list_service_usage_with_order_by_query_param(
     setup_wallets_db,
     mock_list_usage_services,
 ):
+    assert client.app
+
     # without any additional query parameter
     url = client.app.router["list_resource_usage_services"].url_for()
     resp = await client.get(f"{url}")
@@ -237,9 +241,13 @@ async def test_list_service_usage_with_order_by_query_param(
     _, error = await assert_status(resp, status.HTTP_422_UNPROCESSABLE_ENTITY)
     assert mock_list_usage_services.called
     assert error["status"] == status.HTTP_422_UNPROCESSABLE_ENTITY
-    assert error["errors"][0]["message"].startswith(
-        "value is not a valid enumeration member"
-    )
+
+    errors = {(e["code"], e["field"]) for e in error["errors"]}
+    assert {
+        ("value_error", "order_by.field"),
+        ("type_error.enum", "order_by.direction"),
+    } == errors
+    assert len(errors) == 2
 
     # without field
     _filter = {"direction": "asc"}
@@ -253,6 +261,8 @@ async def test_list_service_usage_with_order_by_query_param(
     assert mock_list_usage_services.called
     assert error["status"] == status.HTTP_422_UNPROCESSABLE_ENTITY
     assert error["errors"][0]["message"].startswith("field required")
+    assert error["errors"][0]["code"] == "value_error.missing"
+    assert error["errors"][0]["field"] == "order_by.field"
 
 
 @pytest.mark.parametrize("user_role", [(UserRole.USER)])
@@ -262,6 +272,8 @@ async def test_list_service_usage_with_filters_query_param(
     setup_wallets_db,
     mock_list_usage_services,
 ):
+    assert client.app
+
     # with unable to decode filter query parameter
     url = (
         client.app.router["list_resource_usage_services"]
diff --git a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py
index e2ace9daa6a..c45d2b43783 100644
--- a/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py
+++ b/services/web/server/tests/unit/with_dbs/04/workspaces/test_workspaces.py
@@ -8,6 +8,7 @@
 import pytest
 from aiohttp.test_utils import TestClient
 from models_library.api_schemas_webserver.workspaces import WorkspaceGet
+from models_library.rest_ordering import OrderDirection
 from pytest_simcore.helpers.assert_checks import assert_status
 from pytest_simcore.helpers.webserver_login import UserInfoDict
 from pytest_simcore.helpers.webserver_parametrizations import (
@@ -17,6 +18,26 @@
 from servicelib.aiohttp import status
 from simcore_service_webserver.db.models import UserRole
 from simcore_service_webserver.projects.models import ProjectDict
+from simcore_service_webserver.workspaces._workspaces_handlers import (
+    WorkspacesListQueryParams,
+)
+
+
+def test_workspaces_order_query_model_post_validator():
+
+    # on default
+    query_params = WorkspacesListQueryParams.parse_obj({})
+    assert query_params.order_by
+    assert query_params.order_by.field == "modified"
+    assert query_params.order_by.direction == OrderDirection.DESC
+
+    # on partial default
+    query_params = WorkspacesListQueryParams.parse_obj(
+        {"order_by": {"field": "modified_at"}}
+    )
+    assert query_params.order_by
+    assert query_params.order_by.field == "modified"
+    assert query_params.order_by.direction == OrderDirection.ASC
 
 
 @pytest.mark.parametrize(*standard_role_response(), ids=str)