Repository: podhmo/swagger-marshmallow-codegen

Issue:
treating AdditionalProperties only schema as Dict field
```yaml
definitions:
  person:
    type: object
    properties:
      name:
        type: string
      x-data:
        additionalProperties:
          type: string
```

Discussion:

Patch:
diff --git a/shapes/Makefile b/shapes/Makefile
index 31504c0..37ffc06 100644
--- a/shapes/Makefile
+++ b/shapes/Makefile
@@ -3,8 +3,9 @@ gen:
  defs/00simple-object.py \
  defs/02nesting.py \
  defs/03self-nesting.py \
+ defs/05additionalProperties-without-properties.py \
 
-# 01 and 04 are added by hand.
+# 01,04,06 are added by hand.
 
 gen-candidates:
 	python gen-candidates.py expected > codegen_candidates.py
diff --git a/shapes/codegen_candidates.py b/shapes/codegen_candidates.py
index 7e9d938..b791bfc 100644
--- a/shapes/codegen_candidates.py
+++ b/shapes/codegen_candidates.py
@@ -4,4 +4,6 @@ CANDIDATES = [
     ('nesting', '02nesting.json', '02nesting.py'),
     ('self-nesting', '03self-nesting.json', '03self-nesting.py'),
     ('inline-nesting', '04inline-nesting.json', '04inline-nesting.py'),
+    ('additionalProperties-without-properties', '05additionalProperties-without-properties.json', '05additionalProperties-without-properties.py'),
+    ('additionalProperties', '06additionalProperties.json', '06additionalProperties.py'),
 ]
diff --git a/shapes/defs/05additionalProperties-without-properties.py b/shapes/defs/05additionalProperties-without-properties.py
new file mode 100644
index 0000000..9ee5a54
--- /dev/null
+++ b/shapes/defs/05additionalProperties-without-properties.py
@@ -0,0 +1,8 @@
+from __future__ import annotations
+import typing as t
+
+
+class Person:
+    name: str
+    age: int
+    data: t.Optional[t.Dict[str, str]]
diff --git a/shapes/expected/05additionalProperties-without-properties.py b/shapes/expected/05additionalProperties-without-properties.py
new file mode 100644
index 0000000..ef08466
--- /dev/null
+++ b/shapes/expected/05additionalProperties-without-properties.py
@@ -0,0 +1,10 @@
+from marshmallow import (
+    Schema,
+    fields,
+)
+
+
+class Person(Schema):
+    name = fields.String(required=True)
+    age = fields.Integer(required=True)
+    data = fields.Dict(keys=fields.String(), values=fields.String())
diff --git a/shapes/expected/06additionalProperties.py b/shapes/expected/06additionalProperties.py
new file mode 100644
index 0000000..4ac2a10
--- /dev/null
+++ b/shapes/expected/06additionalProperties.py
@@ -0,0 +1,21 @@
+from marshmallow import (
+    Schema,
+    fields,
+)
+from swagger_marshmallow_codegen.schema import (
+    AdditionalPropertiesSchema,
+    PrimitiveValueSchema,
+)
+
+
+class Box(AdditionalPropertiesSchema):
+    name = fields.String(required=True)
+
+    class Meta:
+        additional_field = fields.Integer()
+
+
+
+class Value(PrimitiveValueSchema):
+    class schema_class(Schema):
+        value = fields.Integer()
diff --git a/shapes/v2/05additionalProperties-without-properties.json b/shapes/v2/05additionalProperties-without-properties.json
new file mode 100644
index 0000000..d406405
--- /dev/null
+++ b/shapes/v2/05additionalProperties-without-properties.json
@@ -0,0 +1,26 @@
+{
+  "version": "2.0.0",
+  "definitions": {
+    "Person": {
+      "type": "object",
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "age": {
+          "type": "integer"
+        },
+        "data": {
+          "type": "object",
+          "additionalProperties": {
+            "type": "string"
+          }
+        }
+      },
+      "required": [
+        "name",
+        "age"
+      ]
+    }
+  }
+}
diff --git a/shapes/v2/06additionalProperties.json b/shapes/v2/06additionalProperties.json
new file mode 100644
index 0000000..23d06dc
--- /dev/null
+++ b/shapes/v2/06additionalProperties.json
@@ -0,0 +1,21 @@
+{
+  "definitions": {
+    "Box": {
+      "additionalProperties": {
+        "$ref": "#/definitions/value"
+      },
+      "properties": {
+        "name": {
+          "type": "string"
+        }
+      },
+      "required": [
+        "name"
+      ]
+    },
+    "value": {
+      "type": "integer"
+    }
+  },
+  "version": "2.0.0"
+}
diff --git a/shapes/v3/05additionalProperties-without-properties.json b/shapes/v3/05additionalProperties-without-properties.json
new file mode 100644
index 0000000..0fb3d61
--- /dev/null
+++ b/shapes/v3/05additionalProperties-without-properties.json
@@ -0,0 +1,28 @@
+{
+  "components": {
+    "schemas": {
+      "Person": {
+        "type": "object",
+        "properties": {
+          "name": {
+            "type": "string"
+          },
+          "age": {
+            "type": "integer"
+          },
+          "data": {
+            "type": "object",
+            "additionalProperties": {
+              "type": "string"
+            }
+          }
+        },
+        "required": [
+          "name",
+          "age"
+        ]
+      }
+    }
+  },
+  "version": "3.x.x"
+}
\ No newline at end of file
diff --git a/shapes/v3/06additionalProperties.json b/shapes/v3/06additionalProperties.json
new file mode 100644
index 0000000..36dc2aa
--- /dev/null
+++ b/shapes/v3/06additionalProperties.json
@@ -0,0 +1,23 @@
+{
+  "components": {
+    "schemas": {
+      "Box": {
+        "additionalProperties": {
+          "$ref": "#/components/schemas/value"
+        },
+        "properties": {
+          "name": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "name"
+        ]
+      },
+      "value": {
+        "type": "integer"
+      }
+    }
+  },
+  "version": "3.x.x"
+}
diff --git a/swagger_marshmallow_codegen/codegen/v2/codegen.py b/swagger_marshmallow_codegen/codegen/v2/codegen.py
index 65c646e..3d70b3f 100644
--- a/swagger_marshmallow_codegen/codegen/v2/codegen.py
+++ b/swagger_marshmallow_codegen/codegen/v2/codegen.py
@@ -35,10 +35,7 @@ class SchemaWriter:
 
     @classmethod
     def override(cls, *, extra_schema_module=None):
-        return partial(
-            cls,
-            extra_schema_module=extra_schema_module,
-        )
+        return partial(cls, extra_schema_module=extra_schema_module,)
 
     def __init__(self, accessor: Accessor, schema_class, *, extra_schema_module=None):
         self.accessor = accessor
@@ -101,6 +98,19 @@ class SchemaWriter:
             value = LazyFormat(
                 "{}(lambda: {}(){})", caller_name, field_class_name, kwargs
             )
+        elif caller_name == "fields.Dict":
+            key_name = "fields.String"
+            value_name = self.accessor.resolver.resolve_caller_name(
+                c, name, field["additionalProperties"]
+            )
+            value = LazyFormat(
+                "{}(keys={}(), values={}(){}{})",
+                caller_name,
+                key_name,
+                value_name,
+                ", " if kwargs else "",
+                kwargs,
+            )
         else:
             if caller_name == "fields.Nested":
                 caller_name = "fields.Field"
diff --git a/swagger_marshmallow_codegen/lifting.py b/swagger_marshmallow_codegen/lifting.py
index c3dbeee..ae1be0f 100644
--- a/swagger_marshmallow_codegen/lifting.py
+++ b/swagger_marshmallow_codegen/lifting.py
@@ -20,11 +20,7 @@ class Flattener:
         typ = data.get("type")
         if typ == "array" and "items" in data:
             return self.on_array_has_items(data, ctx)
-        elif (typ is None or typ == "object") and (
-            from_array
-            or "properties" in data
-            or hasattr(data.get("additionalProperties"), "keys")
-        ):
+        elif (typ is None or typ == "object") and (from_array or "properties" in data):
             return self.on_object_has_properties(data, ctx)
         else:
             return data
diff --git a/swagger_marshmallow_codegen/resolver.py b/swagger_marshmallow_codegen/resolver.py
index 50a350c..2c47434 100644
--- a/swagger_marshmallow_codegen/resolver.py
+++ b/swagger_marshmallow_codegen/resolver.py
@@ -68,12 +68,14 @@ class Resolver:
             return Pair(type="object", format=None)
 
     def resolve_caller_name(self, c, field_name, field):
-        pair = self.resolve_type_and_format(field_name, field)
-        logger.debug("    resolve: type=%s, format=%s", pair.type, pair.format)
-
-        path = self.dispatcher.dispatch(pair, field)
-        if path is None:
-            return None
+        if "additionalProperties" in field and "properties" not in field:
+            path = "marshmallow.fields:Dict"
+        else:
+            pair = self.resolve_type_and_format(field_name, field)
+            logger.debug("    resolve: type=%s, format=%s", pair.type, pair.format)
+            path = self.dispatcher.dispatch(pair, field)
+            if path is None:
+                return None
 
         module, cls_name = path.rsplit(":", 1)
         if module == "marshmallow.fields":


Test patch:
diff --git a/shapes/test_schema.py b/shapes/test_schema.py
index 3468d7e..011ac1f 100644
--- a/shapes/test_schema.py
+++ b/shapes/test_schema.py
@@ -36,3 +36,39 @@ def test_03():
         "mother": {"name": "boo"},
     }
     s.load(data)
+
+
+def test_04():
+    m = import_module("./expected/04inline-nesting.py", cwd=False)
+    s = m.Person()
+    data = {
+        "name": "foo",
+        "age": 20,
+        "memo": {"title": "hello", "content": "hello world"},
+    }
+    s.load(data)
+
+
+def test_05():
+    m = import_module(
+        "./expected/05additionalProperties-without-properties.py", cwd=False
+    )
+    s = m.Person()
+    data = {
+        "name": "foo",
+        "age": 20,
+        "data": {"title": "hello", "content": "hello world"},
+    }
+    s.load(data)
+
+
+def test_06():
+    m = import_module("./expected/06additionalProperties.py", cwd=False)
+    s = m.Box()
+    data = {
+        "name": "foo",
+        "x": 1,
+        "y": 20,
+        "z": 300,
+    }
+    s.load(data)
diff --git a/swagger_marshmallow_codegen/tests/codegen_candidates.py b/swagger_marshmallow_codegen/tests/codegen_candidates.py
index 7e9d938..b791bfc 100644
--- a/swagger_marshmallow_codegen/tests/codegen_candidates.py
+++ b/swagger_marshmallow_codegen/tests/codegen_candidates.py
@@ -4,4 +4,6 @@ CANDIDATES = [
     ('nesting', '02nesting.json', '02nesting.py'),
     ('self-nesting', '03self-nesting.json', '03self-nesting.py'),
     ('inline-nesting', '04inline-nesting.json', '04inline-nesting.py'),
+    ('additionalProperties-without-properties', '05additionalProperties-without-properties.json', '05additionalProperties-without-properties.py'),
+    ('additionalProperties', '06additionalProperties.json', '06additionalProperties.py'),
 ]
diff --git a/swagger_marshmallow_codegen/tests/legacy_dst/00additional.py b/swagger_marshmallow_codegen/tests/legacy_dst/00additional.py
index 0db7e86..2977697 100644
--- a/swagger_marshmallow_codegen/tests/legacy_dst/00additional.py
+++ b/swagger_marshmallow_codegen/tests/legacy_dst/00additional.py
@@ -30,10 +30,4 @@ class Box3(AdditionalPropertiesSchema):
 
 
 class Box4(Schema):
-    box = fields.Nested(lambda: Box4Box())
-
-
-class Box4Box(AdditionalPropertiesSchema):
-
-    class Meta:
-        additional_field = fields.String()
+    box = fields.Dict(keys=fields.String(), values=fields.String())