Repository: sanic-org/sanic-ext (python)

Issue:
OpenAPI extension parses dataclass validators as body
```python
#from pydantic.dataclasses import dataclass
from pydantic.dataclasses import dataclass

@dataclass
class RegisterBody:
    name: str
    email: str
    password: str

    @validator('name')
    def validate_name(cls, v):
        if len(v) < 5:
            raise ValueError('name must be at leasth 5 characters')

@app.post("/register")
@openapi.body({"application/json": RegisterBody})
@validate(json=RegisterBody)
async def register(request, body):
     return json({})
```

The code above produces the OpenAPI `/docs` page to populate a `validator_name` field in the Swagger UI, while for the actual mode it is not a field. Tried with the builting python dataclasses and the Pydantic wrapper too.

![image](https://user-images.githubusercontent.com/3187667/172001173-f0dbb421-3a20-4a2b-9578-9a97a8ade9b3.png)

Sanic v22.3.2
Sanic-ext v22.3.2
Arch Linux x86_64
Python 3.10.4


Pull request description:
Better serialization of objects
Closes #89 

Patch:
diff --git a/sanic_ext/extensions/openapi/types.py b/sanic_ext/extensions/openapi/types.py
index 3bf53ef..191132d 100644
--- a/sanic_ext/extensions/openapi/types.py
+++ b/sanic_ext/extensions/openapi/types.py
@@ -2,6 +2,7 @@ import json
 import uuid
 from datetime import date, datetime, time
 from enum import Enum
+from inspect import getmembers, isfunction
 from typing import (
     Any,
     Dict,
@@ -308,7 +309,11 @@ def _serialize(value) -> Any:
 
 def _properties(value: object) -> Dict:
     try:
-        fields = {x: v for x, v in value.__dict__.items()}
+        fields = {
+            x: val
+            for x, v in getmembers(value, _is_property)
+            if (val := _extract(v))
+        }
     except AttributeError:
         fields = {}
 
@@ -318,3 +323,14 @@ def _properties(value: object) -> Dict:
         for k, v in {**get_type_hints(cls), **fields}.items()
         if not k.startswith("_")
     }
+
+
+def _extract(item):
+    if isinstance(item, property):
+        hints = get_type_hints(item.fget)
+        return hints.get("return")
+    return item
+
+
+def _is_property(item):
+    return not isfunction(item)


Test patch:
diff --git a/tests/extensions/openapi/test_schema.py b/tests/extensions/openapi/test_schema.py
index 415691f..c6033a7 100644
--- a/tests/extensions/openapi/test_schema.py
+++ b/tests/extensions/openapi/test_schema.py
@@ -12,8 +12,17 @@ def test_schema_list():
         list1: List[int]
         list2: list[int]
 
+        def no_show(self) -> None:
+            ...
+
+        @property
+        def show(self) -> bool:
+            return True
+
     schema = Schema.make(Foo)
-    schema.serialize() == {
+    serialized = schema.serialize()
+    assert "no_show" not in serialized
+    assert serialized == {
         "type": "object",
         "properties": {
             "list1": {
@@ -24,5 +33,6 @@ def test_schema_list():
                 "type": "array",
                 "items": {"type": "integer", "format": "int32"},
             },
+            "show": {"type": "boolean"},
         },
     }