Add `Request.json` helper method and update the docs and README to reflect

Commit c7af233 · patx · 2026-02-12T19:24:26-05:00

Changeset
c7af2332994d067e635db7ae1703cb6f7ea48d04
Parents
17aa397b2f03d1ffaee8c52bec6c66fb6ebdda3c

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 9ea53b0..287c6c3 100644
--- a/README.md
+++ b/README.md
@@ -108,18 +108,16 @@ Access your app at [http://127.0.0.1:8000](http://127.0.0.1:8000).
 
 ### **Route Handlers**
 
-MicroPie's route handlers map URLs to methods in your `App` subclass, handling HTTP requests with flexible parameter mapping and response formats.
+MicroPie's route handlers map URLs to methods in your `App` subclass. Handler input can come from automatic argument binding or request helper methods.
 
 #### **Key Points**
 - **Automatic Mapping**: URLs map to method names (e.g., `/greet` → `greet`, `/` → `index`).
 - **Private Methods**: Methods starting with `_` (e.g., `_private_method`) are private and inaccessible via URLs, returning 404. **Security Note**: Use `_` for sensitive methods to prevent external access.
-- **Parameters**: Automatically populated from:
-  - Path segments (e.g., `/greet/Alice` → `name="Alice"`).
-  - Query strings (e.g., `?name=Alice`).
-  - Form data (POST/PUT/PATCH).
-  - Session data (`self.request.session`).
-  - File uploads (`self.request.files`).
-  - Default values in method signatures.
+- **Automatic Argument Binding**: Handler args are populated from path/query/body data by parameter name.
+- **Request Helpers**:
+  - `self.request.query(name, default)` for query-string values.
+  - `self.request.form(name, default)` for form/body values.
+  - `self.request.json()` for full JSON payloads, or `self.request.json(name, default)` for a key lookup.
 - **HTTP Methods**: Handlers support all methods (GET, POST, etc.). Check `self.request.method` to handle specific methods.
 - **Responses**:
   - String, bytes, or JSON-serializable object.
@@ -131,36 +129,45 @@ MicroPie's route handlers map URLs to methods in your `App` subclass, handling H
 - **Errors**: Auto-handled 404/400; customize via middleware.
 - **Dynamic Params**: Use `*args` for multiple path parameters.
 
-#### **Flexible HTTP Routing for GET Requests**
-MicroPie automatically maps URLs to methods within your `App` class. Routes can be defined as either synchronous or asynchronous functions, offering good flexibility.
-
-For GET requests, pass data through query strings or URL path segments, automatically mapped to method arguments.
+#### **Automatic Argument Binding**
+MicroPie can bind handler parameters directly from incoming request data:
 ```python
 class MyApp(App):
     async def greet(self, name="Guest"):
         return f"Hello, {name}!"
 
-    async def hello(self):
-        name = self.request.query("name")
-        return f"Hello {name}!"
+    async def submit(self, username="Anonymous"):
+        return f"Submitted by: {username}"
 ```
 **Access:**
-- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) returns `Hello, Alice!`, same as [http://127.0.0.1:8000/greet/Alice](http://127.0.0.1:8000/greet/Alice) returns `Hello, Alice!`.
-- [http://127.0.0.1:8000/hello/Alice](http://127.0.0.1:8000/hello/Alice) returns a `500 Internal Server Error` because it is expecting [http://127.0.0.1:8000/hello?name=Alice](http://127.0.0.1:8000/hello?name=Alice), which returns `Hello Alice!`
+- [http://127.0.0.1:8000/greet/Alice](http://127.0.0.1:8000/greet/Alice) returns `Hello, Alice!`.
+- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) also returns `Hello, Alice!`.
+- POST `application/x-www-form-urlencoded` to `/submit` with `username=bob` returns `Submitted by: bob`.
+- POST `application/json` to `/submit` with `{"username": "bob"}` also returns `Submitted by: bob`.
 
-#### **Flexible HTTP POST Request Handling**
-MicroPie also supports handling form data submitted via HTTP POST requests. Form data is automatically mapped to method arguments. It is able to handle default values and raw/JSON POST data:
+#### **Helper-Based Request Access**
+Use helper methods for query, form, and JSON payload access:
 ```python
 class MyApp(App):
-    async def submit_default_values(self, username="Anonymous"):
-        return f"Form submitted by: {username}"
+    async def greet(self):
+        name = self.request.query("name", "Guest")
+        return f"Hello, {name}!"
 
-    async def submit_catch_all(self):
+    async def submit(self):
         username = self.request.form("username", "Anonymous")
         return f"Submitted by: {username}"
+
+    async def submit_json(self):
+        data = self.request.json()
+        username = self.request.json("username", "Anonymous")
+        return {"submitted_by": username, "raw": data}
 ```
+**Access:**
+- [http://127.0.0.1:8000/greet?name=Alice](http://127.0.0.1:8000/greet?name=Alice) returns `Hello, Alice!`.
+- POST `application/x-www-form-urlencoded` to `/submit` with `username=bob` returns `Submitted by: bob`.
+- POST `application/json` to `/submit_json` with `{"username": "bob"}` returns JSON including `submitted_by: "bob"`.
 
-By default, MicroPie's route handlers can accept any request method, it's up to you how to handle any incoming requests! You can check the request method (and a number of other things specific to the current request state) in the handler with `self.request.method`. You can see how to handle POST JSON data at [examples/json_api](https://github.com/patx/micropie/tree/main/examples/json_api).
+By default, MicroPie's route handlers can accept any request method. Check `self.request.method` in a handler when route behavior differs by method. For lower-level request internals such as `query_params`, `body_params`, and `get_json`, see `docs/apidocs/reference/request.rst`.
 
 ### **Lifecycle Event Handling**
 MicroPie supports ASGI lifespan events, allowing you to register asynchronous handlers for application startup and shutdown. This is useful for tasks like initializing database connections or cleaning up resources.
diff --git a/docs/apidocs/reference/request.rst b/docs/apidocs/reference/request.rst
index 33c6e6c..4f752f4 100644
--- a/docs/apidocs/reference/request.rst
+++ b/docs/apidocs/reference/request.rst
@@ -35,7 +35,8 @@ Request class
    .. attribute:: query_params
 
       A ``dict`` mapping each query parameter to a list of values.
-      For convenience, use :meth:`~micropie.Request.query` to obtain
+      This is the raw parsed mapping from the query string.  For
+      convenience, use :meth:`~micropie.Request.query` to obtain
       the first value.
 
    .. method:: query(name, default=None)
@@ -48,7 +49,8 @@ Request class
       A ``dict`` mapping form field names to lists of values.  For
       JSON requests, body parameters are derived from the top‑level
       object; for ``application/x-www-form-urlencoded`` forms they are
-      parsed using :func:`urllib.parse.parse_qs`.
+      parsed using :func:`urllib.parse.parse_qs`.  This is the raw
+      mapping used by MicroPie for argument binding.
 
    .. method:: form(name, default=None)
 
@@ -61,6 +63,16 @@ Request class
       the request contains valid JSON with content type
       ``application/json``.
 
+   .. method:: json(name=None, default=None)
+
+      Return the parsed JSON payload or a value from a top-level JSON
+      object.
+
+      If *name* is omitted, this method returns the full parsed payload
+      (equivalent to :attr:`~micropie.Request.get_json`).  If *name* is
+      provided and the payload is an object, the value for that key is
+      returned; otherwise *default* is returned.
+
    .. attribute:: session
 
       A ``dict`` for storing per‑client data across requests.  See
diff --git a/docs/apidocs/tutorial/routing.rst b/docs/apidocs/tutorial/routing.rst
index 77418dd..58d5588 100644
--- a/docs/apidocs/tutorial/routing.rst
+++ b/docs/apidocs/tutorial/routing.rst
@@ -65,6 +65,36 @@ sources in the following order:
 If MicroPie cannot determine a value for a required parameter, it
 returns a ``400 Bad Request``.
 
+Helpers and raw request data
+----------------------------
+
+In handlers, you can access request input through helper methods or
+through raw parsed attributes:
+
+* Helpers:
+
+  * ``self.request.query(name, default=None)`` returns the first query
+    value.
+  * ``self.request.form(name, default=None)`` returns the first
+    form/body value.
+  * ``self.request.json(name=None, default=None)`` returns either the
+    full parsed JSON payload or a key from a top-level JSON object.
+
+* Raw attributes:
+
+  * ``self.request.path_params``: list of positional path segments.
+  * ``self.request.query_params``: ``dict[str, list[str]]`` parsed from
+    the query string.
+  * ``self.request.body_params``: ``dict[str, list[str]]`` parsed from
+    form-urlencoded payloads, or mirrored from top-level JSON objects.
+  * ``self.request.get_json``: full parsed JSON payload.
+  * ``self.request.session``: session dictionary.
+  * ``self.request.files``: uploaded multipart files.
+
+The helper APIs are convenient when you need a single value.  The raw
+attributes are useful when you need full multi-value access or exact
+payload inspection.
+
 Examples
 --------
 
diff --git a/micropie.py b/micropie.py
index 22ae68d..01cb3ad 100644
--- a/micropie.py
+++ b/micropie.py
@@ -163,6 +163,20 @@ class Request:
             return values[0]
         return default
 
+    def json(self, name: Optional[str] = None, default: Any = None) -> Any:
+        """
+        Return the parsed JSON body or a value from a top-level JSON object.
+
+        Args:
+            name: Optional key from the top-level JSON object.
+            default: Value returned when key is missing or payload is not an object.
+        """
+        if name is None:
+            return self.get_json
+        if isinstance(self.get_json, dict):
+            return self.get_json.get(name, default)
+        return default
+
 
 class WebSocketRequest(Request):
     """Represents a WebSocket request in the MicroPie framework."""
diff --git a/tests.py b/tests.py
index 3a0d345..f1d815d 100644
--- a/tests.py
+++ b/tests.py
@@ -112,6 +112,34 @@ class TestRequest(MicroPieTestCase):
         self.assertIsNone(request.form("missing"))
         self.assertEqual(request.form("missing", "fallback"), "fallback")
 
+    async def test_request_json_helper(self):
+        """Verify JSON helper returns payloads, keys, and defaults."""
+        request = Request(
+            {
+                "type": "http",
+                "method": "POST",
+                "path": "/json",
+                "headers": [],
+                "get_json": {"username": "alice", "age": 42},
+            }
+        )
+        self.assertEqual(request.json(), {"username": "alice", "age": 42})
+        self.assertEqual(request.json("username"), "alice")
+        self.assertIsNone(request.json("missing"))
+        self.assertEqual(request.json("missing", "fallback"), "fallback")
+
+        list_payload_request = Request(
+            {
+                "type": "http",
+                "method": "POST",
+                "path": "/json",
+                "headers": [],
+                "get_json": ["a", "b"],
+            }
+        )
+        self.assertEqual(list_payload_request.json(), ["a", "b"])
+        self.assertEqual(list_payload_request.json("username", "fallback"), "fallback")
+
 
 class TestSession(MicroPieTestCase):
     """Tests for session management and cookie parsing."""