Repository: vitalik/django-ninja

Issue:
mixing query param bug
<img width="1089" alt="CleanShot 2021-07-21 at 15 23 44@2x" src="https://user-images.githubusercontent.com/95222/126487840-33ec0cf2-5978-4a1f-816e-73afe970561b.png">

this works

but this

<img width="1157" alt="CleanShot 2021-07-21 at 15 24 51@2x" src="https://user-images.githubusercontent.com/95222/126488014-853ee9bf-9574-49a4-b5a3-b11cc3ac9606.png">


not:

<img width="718" alt="CleanShot 2021-07-21 at 15 24 17@2x" src="https://user-images.githubusercontent.com/95222/126487860-88b282ff-2914-4d3d-8bb9-e1fa8552e178.png">


Discussion:

Patch:
diff --git a/ninja/params_models.py b/ninja/params_models.py
index e8f8c7c..2ddabe6 100644
--- a/ninja/params_models.py
+++ b/ninja/params_models.py
@@ -48,6 +48,15 @@ class ParamModel(BaseModel, ABC):
         varname = getattr(cls, "_single_attr", None)
         if varname:
             data = {varname: data}
+
+        mixed_attrs = getattr(cls, "_mixed_attrs", None)
+        if mixed_attrs:
+            for param_name, varname in mixed_attrs.items():
+                if varname not in data:
+                    data[varname] = {}
+                if param_name in data:
+                    data[varname][param_name] = data.pop(param_name)
+
         # TODO: I guess if data is not dict - raise an HttpBadRequest
         return cls(**data)
 
diff --git a/ninja/signature/details.py b/ninja/signature/details.py
index 6659a76..5e0d51f 100644
--- a/ninja/signature/details.py
+++ b/ninja/signature/details.py
@@ -34,9 +34,9 @@ class ViewSignature:
         self.params = []
         for name, arg in self.signature.parameters.items():
             if name == "request":
-                # TODO: maybe better assert that 1st param is request  or check by type?
-                # maybe even  have attribute like `has_request`
-                # so that users can ignroe passing request if not needed
+                # TODO: maybe better assert that 1st param is request or check by type?
+                # maybe even have attribute like `has_request`
+                # so that users can ignore passing request if not needed
                 continue
 
             if arg.kind == arg.VAR_KEYWORD:
@@ -69,6 +69,23 @@ class ViewSignature:
                 if cls._in() == "body" or is_pydantic_model(args[0].annotation):
                     attrs["_single_attr"] = args[0].name
 
+            elif cls._in() == "query":
+                pydantic_models = [
+                    arg for arg in args if is_pydantic_model(arg.annotation)
+                ]
+                if pydantic_models:
+                    mixed_attrs = {}
+                    for modeled_attr in pydantic_models:
+                        for (
+                            attr_name,
+                            field,
+                        ) in modeled_attr.annotation.__fields__.items():
+                            mixed_attrs[attr_name] = modeled_attr.name
+                            if hasattr(field, "alias"):
+                                mixed_attrs[field.alias] = modeled_attr.name
+
+                    attrs["_mixed_attrs"] = mixed_attrs
+
             # adding annotations:
             attrs["__annotations__"] = {i.name: i.annotation for i in args}
 


Test patch:
diff --git a/tests/test_query_schema.py b/tests/test_query_schema.py
index db37f0c..fca70c0 100644
--- a/tests/test_query_schema.py
+++ b/tests/test_query_schema.py
@@ -1,6 +1,7 @@
 from datetime import datetime
 from enum import IntEnum
 
+import pytest
 from pydantic import Field
 
 from ninja import NinjaAPI, Query, Schema, files
@@ -20,6 +21,11 @@ class Filter(Schema):
     range: Range = Range.TWENTY
 
 
+class Data(Schema):
+    an_int: int = Field(alias="int", default=0)
+    a_float: float = Field(alias="float", default=1.5)
+
+
 api = NinjaAPI()
 
 
@@ -28,6 +34,17 @@ def query_params_schema(request, filters: Filter = Query(...)):
     return filters.dict()
 
 
+@api.get("/test-mixed")
+def query_params_mixed_schema(
+    request,
+    query1: int,
+    query2: int = 5,
+    filters: Filter = Query(...),
+    data: Data = Query(...),
+):
+    return dict(query1=query1, query2=query2, filters=filters.dict(), data=data.dict())
+
+
 def test_request():
     client = TestClient(api)
     response = client.get("/test?from=1&to=2&range=20&foo=1&range2=50")
@@ -42,6 +59,42 @@ def test_request():
     assert response.status_code == 422
 
 
+def test_request_mixed():
+    client = TestClient(api)
+    response = client.get(
+        "/test-mixed?from=1&to=2&range=20&foo=1&range2=50&query1=2&int=3&float=1.6"
+    )
+    print(response.json())
+    assert response.json() == {
+        "data": {"a_float": 1.6, "an_int": 3},
+        "filters": {
+            "from_datetime": "1970-01-01T00:00:01Z",
+            "range": 20,
+            "to_datetime": "1970-01-01T00:00:02Z",
+        },
+        "query1": 2,
+        "query2": 5,
+    }
+
+    response = client.get(
+        "/test-mixed?from=1&to=2&range=20&foo=1&range2=50&query1=2&query2=10"
+    )
+    print(response.json())
+    assert response.json() == {
+        "data": {"a_float": 1.5, "an_int": 0},
+        "filters": {
+            "from_datetime": "1970-01-01T00:00:01Z",
+            "range": 20,
+            "to_datetime": "1970-01-01T00:00:02Z",
+        },
+        "query1": 2,
+        "query2": 10,
+    }
+
+    response = client.get("/test-mixed?from=1&to=2")
+    assert response.status_code == 422
+
+
 def test_schema():
     schema = api.get_openapi_schema()
     params = schema["paths"]["/api/test"]["get"]["parameters"]