Repository: podhmo/swagger-marshmallow-codegen

Issue:
separated output

Discussion:

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index c8965d7..2c4fecd 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,3 +1,8 @@
+0.6.2
+
+- support nested additionalProperties
+- fix guessing OAS version detection
+
 0.6.1
 
 - fix bug at additionalProperties
diff --git a/VERSION b/VERSION
index 7ceb040..b1d7abc 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.6.1
\ No newline at end of file
+0.6.2
\ No newline at end of file
diff --git a/examples/07custom/Makefile b/examples/x00custom/Makefile
similarity index 100%
rename from examples/07custom/Makefile
rename to examples/x00custom/Makefile
diff --git a/examples/07custom/__init__.py b/examples/x00custom/__init__.py
similarity index 100%
rename from examples/07custom/__init__.py
rename to examples/x00custom/__init__.py
diff --git a/examples/07custom/_custom.py b/examples/x00custom/_custom.py
similarity index 100%
rename from examples/07custom/_custom.py
rename to examples/x00custom/_custom.py
diff --git a/examples/07custom/main.py b/examples/x00custom/main.py
similarity index 100%
rename from examples/07custom/main.py
rename to examples/x00custom/main.py
diff --git a/examples/07custom/myschema.py b/examples/x00custom/myschema.py
similarity index 100%
rename from examples/07custom/myschema.py
rename to examples/x00custom/myschema.py
diff --git a/examples/07custom/person.py b/examples/x00custom/person.py
similarity index 100%
rename from examples/07custom/person.py
rename to examples/x00custom/person.py
diff --git a/examples/07custom/person.yaml b/examples/x00custom/person.yaml
similarity index 100%
rename from examples/07custom/person.yaml
rename to examples/x00custom/person.yaml
diff --git a/examples/x01separated/Makefile b/examples/x01separated/Makefile
new file mode 100644
index 0000000..3882f3f
--- /dev/null
+++ b/examples/x01separated/Makefile
@@ -0,0 +1,19 @@
+GREP := $(shell which ggrep || which grep)
+
+default: clean
+	$(MAKE) $(shell cat Makefile | $(GREP) -P '^[0-9]+:' | tr ':' ' ')
+
+clean:
+	rm -rf petstore petstore_extend
+
+# simple
+00:
+	swagger-marshmallow-codegen petstore.json -d petstore
+
+# with simple relation
+01:
+	swagger-marshmallow-codegen petstore-extend.json -d petstore-extend
+
+# with mutual reference
+02:
+	swagger-marshmallow-codegen team-and-user.json -d team-and-user
diff --git a/examples/x01separated/petstore-extend.json b/examples/x01separated/petstore-extend.json
new file mode 100644
index 0000000..7fe04bb
--- /dev/null
+++ b/examples/x01separated/petstore-extend.json
@@ -0,0 +1,161 @@
+{
+  "components": {
+    "schemas": {
+      "id": {
+        "type": "string",
+        "description": "ObjectId",
+        "example": "541a9fd949b5ac0b00000000",
+        "pattern": "[0-9a-f]{24}"
+      },
+      "errorCategory": {
+        "type": "string",
+        "enum": ["API", "Internal", "Bug", "Human"]
+      },
+      "errorCategories": {
+        "type": "array",
+        "items": {
+          "$ref": "#/components/schemas/errorCategory"
+        }
+      },
+      "Error": {
+        "properties": {
+          "code": {
+            "format": "int32",
+            "type": "integer"
+          },
+          "message": {
+            "type": "string"
+          },
+          "categoris": {
+            "$ref": "#/components/schemas/errorCategories"
+          }
+        },
+        "required": [
+          "code",
+          "message"
+        ],
+        "type": "object"
+      },
+      "Pet": {
+        "properties": {
+          "father": {
+            "$ref": "#/components/schemas/Pet"
+          },
+          "id": {
+            "$ref": "#/components/schemas/id"
+          },
+          "mother": {
+            "$ref": "#/components/schemas/Pet"
+          },
+          "name": {
+            "type": "string"
+          },
+          "shop": {
+            "$ref": "#/components/schemas/Shop"
+          },
+          "owners": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/Owner"
+            }
+          },
+          "tag": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "id",
+          "name"
+        ],
+        "type": "object"
+      },
+      "Pets": {
+        "items": {
+          "$ref": "#/components/schemas/Pet"
+        },
+        "type": "array"
+      },
+      "Cat": {
+        "description": "A representation of a cat",
+        "required": ["huntingSkill"],
+        "allOf": [
+          {"$ref": "#/components/schemas/Pet"},
+          {
+            "type": "object",
+            "properties": {
+              "huntingSkill": {
+                "type": "string",
+                "description": "The measured skill for hunting",
+                "default": "lazy",
+                "enum": ["clueless", "lazy", "adventurous", "aggressive"]
+              }
+            }
+          }
+        ]
+      },
+      "Dog": {
+        "description": "A representation of a dog",
+        "required": ["packSize"],
+        "allOf": [
+          {"$ref": "#/components/schemas/Pet"},
+          {
+            "type": "object",
+            "properties": {
+              "packSize": {
+                "type": "integer",
+                "description": "the size of the pack the dog is from",
+                "default": 0,
+                "minimum": 0
+              }
+            }
+          }
+        ]
+      },
+      "Shop": {
+        "properties": {
+          "id": {
+            "$ref": "#/components/schemas/id"
+          },
+          "name": {
+            "type": "string"
+          },
+          "description": {
+            "properties": {
+              "content": {
+                "type": "string"
+              },
+              "position": {
+                "type": "string"
+              }
+            },
+            "type": "object"
+          }
+        },
+        "required": [
+          "id",
+          "name"
+        ],
+        "type": "object"
+      },
+      "Owner": {
+        "properties": {
+          "id": {
+            "$ref": "#/components/schemas/id"
+          },
+          "name": {
+            "type": "string"
+          },
+          "pets": {
+            "$ref": "#/components/schemas/Pets"
+          }
+        },
+        "required": [
+          "id",
+          "name"
+        ],
+        "type": "object"
+      }
+    }
+  },
+  "openapi": "3.0.0"
+}
diff --git a/examples/x01separated/petstore.json b/examples/x01separated/petstore.json
new file mode 100644
index 0000000..5eca459
--- /dev/null
+++ b/examples/x01separated/petstore.json
@@ -0,0 +1,48 @@
+{
+  "openapi": "3.0.0",
+  "components": {
+    "schemas": {
+      "Pet": {
+        "type": "object",
+        "required": [
+          "id",
+          "name"
+        ],
+        "properties": {
+          "id": {
+            "type": "integer",
+            "format": "int64"
+          },
+          "name": {
+            "type": "string"
+          },
+          "tag": {
+            "type": "string"
+          }
+        }
+      },
+      "Pets": {
+        "type": "array",
+        "items": {
+          "$ref": "#/components/schemas/Pet"
+        }
+      },
+      "Error": {
+        "type": "object",
+        "required": [
+          "code",
+          "message"
+        ],
+        "properties": {
+          "code": {
+            "type": "integer",
+            "format": "int32"
+          },
+          "message": {
+            "type": "string"
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/x01separated/petstore/Error.py b/examples/x01separated/petstore/Error.py
new file mode 100644
index 0000000..956f8a5
--- /dev/null
+++ b/examples/x01separated/petstore/Error.py
@@ -0,0 +1,15 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+
+
+class Error(Schema):
+    code = fields.Integer(required=True)
+    message = fields.String(required=True)
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore/Pet.py b/examples/x01separated/petstore/Pet.py
new file mode 100644
index 0000000..d7939d6
--- /dev/null
+++ b/examples/x01separated/petstore/Pet.py
@@ -0,0 +1,16 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+
+
+class Pet(Schema):
+    id = fields.Integer(required=True)
+    name = fields.String(required=True)
+    tag = fields.String()
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore/__init__.py b/examples/x01separated/petstore/__init__.py
new file mode 100644
index 0000000..20c9fde
--- /dev/null
+++ b/examples/x01separated/petstore/__init__.py
@@ -0,0 +1,2 @@
+from .Error import Error
+from .Pet import Pet
diff --git a/examples/x01separated/petstore/_lazy.py b/examples/x01separated/petstore/_lazy.py
new file mode 100644
index 0000000..d360fc0
--- /dev/null
+++ b/examples/x01separated/petstore/_lazy.py
@@ -0,0 +1,8 @@
+def _useError():
+    from .Error import Error
+    return Error
+
+
+def _usePet():
+    from .Pet import Pet
+    return Pet
diff --git a/examples/x01separated/petstore_extend/Cat.py b/examples/x01separated/petstore_extend/Cat.py
new file mode 100644
index 0000000..b42bd1c
--- /dev/null
+++ b/examples/x01separated/petstore_extend/Cat.py
@@ -0,0 +1,18 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+from marshmallow.validate import OneOf
+from ._lazy import _usePet
+from .Pet import Pet
+
+
+class Cat(Pet):
+    """A representation of a cat"""
+    huntingSkill = fields.String(required=True, description='The measured skill for hunting', validate=[OneOf(choices=['clueless', 'lazy', 'adventurous', 'aggressive'], labels=[])])
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore_extend/Dog.py b/examples/x01separated/petstore_extend/Dog.py
new file mode 100644
index 0000000..8749b2a
--- /dev/null
+++ b/examples/x01separated/petstore_extend/Dog.py
@@ -0,0 +1,18 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+from swagger_marshmallow_codegen.validate import Range
+from ._lazy import _usePet
+from .Pet import Pet
+
+
+class Dog(Pet):
+    """A representation of a dog"""
+    packSize = fields.Integer(required=True, description='the size of the pack the dog is from', validate=[Range(min=0, max=None, min_inclusive=True, max_inclusive=True)])
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore_extend/Error.py b/examples/x01separated/petstore_extend/Error.py
new file mode 100644
index 0000000..c890f6e
--- /dev/null
+++ b/examples/x01separated/petstore_extend/Error.py
@@ -0,0 +1,17 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+from marshmallow.validate import OneOf
+
+
+class Error(Schema):
+    code = fields.Integer(required=True)
+    message = fields.String(required=True)
+    categoris = fields.List(fields.String(validate=[OneOf(choices=['API', 'Internal', 'Bug', 'Human'], labels=[])]))
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore_extend/Owner.py b/examples/x01separated/petstore_extend/Owner.py
new file mode 100644
index 0000000..b460c0c
--- /dev/null
+++ b/examples/x01separated/petstore_extend/Owner.py
@@ -0,0 +1,19 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+import re
+from marshmallow.validate import Regexp
+from ._lazy import _usePet
+
+
+class Owner(Schema):
+    id = fields.String(required=True, description='ObjectId', validate=[Regexp(regex=re.compile('[0-9a-f]{24}'))])
+    name = fields.String(required=True)
+    pets = fields.List(fields.Nested(_usePet))
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore_extend/Pet.py b/examples/x01separated/petstore_extend/Pet.py
new file mode 100644
index 0000000..2937085
--- /dev/null
+++ b/examples/x01separated/petstore_extend/Pet.py
@@ -0,0 +1,26 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+import re
+from marshmallow.validate import Regexp
+from ._lazy import (
+    _useShop,
+    _useOwner,
+)
+
+
+class Pet(Schema):
+    father = fields.Nested(lambda: Pet())
+    id = fields.String(required=True, description='ObjectId', validate=[Regexp(regex=re.compile('[0-9a-f]{24}'))])
+    mother = fields.Nested(lambda: Pet())
+    name = fields.String(required=True)
+    shop = fields.Nested(_useShop)
+    owners = fields.List(fields.Nested(_useOwner))
+    tag = fields.String()
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore_extend/Shop.py b/examples/x01separated/petstore_extend/Shop.py
new file mode 100644
index 0000000..b5b6503
--- /dev/null
+++ b/examples/x01separated/petstore_extend/Shop.py
@@ -0,0 +1,27 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+import re
+from marshmallow.validate import Regexp
+
+
+class Shop(Schema):
+    id = fields.String(required=True, description='ObjectId', validate=[Regexp(regex=re.compile('[0-9a-f]{24}'))])
+    name = fields.String(required=True)
+    description = fields.Nested(lambda: ShopDescription())
+
+    class Meta:
+        unknown = INCLUDE
+
+
+
+class ShopDescription(Schema):
+    content = fields.String()
+    position = fields.String()
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/petstore_extend/__init__.py b/examples/x01separated/petstore_extend/__init__.py
new file mode 100644
index 0000000..117ff7f
--- /dev/null
+++ b/examples/x01separated/petstore_extend/__init__.py
@@ -0,0 +1,6 @@
+from .Cat import Cat
+from .Dog import Dog
+from .Error import Error
+from .Owner import Owner
+from .Pet import Pet
+from .Shop import Shop
diff --git a/examples/x01separated/petstore_extend/_lazy.py b/examples/x01separated/petstore_extend/_lazy.py
new file mode 100644
index 0000000..a37c5d4
--- /dev/null
+++ b/examples/x01separated/petstore_extend/_lazy.py
@@ -0,0 +1,28 @@
+def _useCat():
+    from .Cat import Cat
+    return Cat
+
+
+def _useDog():
+    from .Dog import Dog
+    return Dog
+
+
+def _useError():
+    from .Error import Error
+    return Error
+
+
+def _useOwner():
+    from .Owner import Owner
+    return Owner
+
+
+def _usePet():
+    from .Pet import Pet
+    return Pet
+
+
+def _useShop():
+    from .Shop import Shop
+    return Shop
diff --git a/examples/x01separated/team-and-user.json b/examples/x01separated/team-and-user.json
new file mode 100644
index 0000000..c544425
--- /dev/null
+++ b/examples/x01separated/team-and-user.json
@@ -0,0 +1,27 @@
+{
+  "definitions": {
+    "team": {
+      "properties": {
+        "members": {
+          "items": {
+            "$ref": "#/definitions/user"
+          },
+          "type": "array"
+        },
+        "name": {
+          "type": "string"
+        }
+      }
+    },
+    "user": {
+      "properties": {
+        "name": {
+          "type": "string"
+        },
+        "team": {
+          "$ref": "#/definitions/team"
+        }
+      }
+    }
+  }
+}
diff --git a/examples/x01separated/team_and_user/__init__.py b/examples/x01separated/team_and_user/__init__.py
new file mode 100644
index 0000000..60aef13
--- /dev/null
+++ b/examples/x01separated/team_and_user/__init__.py
@@ -0,0 +1,2 @@
+from .team import Team
+from .user import User
diff --git a/examples/x01separated/team_and_user/_lazy.py b/examples/x01separated/team_and_user/_lazy.py
new file mode 100644
index 0000000..48af637
--- /dev/null
+++ b/examples/x01separated/team_and_user/_lazy.py
@@ -0,0 +1,8 @@
+def _useTeam():
+    from .team import Team
+    return Team
+
+
+def _useUser():
+    from .user import User
+    return User
diff --git a/examples/x01separated/team_and_user/team.py b/examples/x01separated/team_and_user/team.py
new file mode 100644
index 0000000..e9df3ba
--- /dev/null
+++ b/examples/x01separated/team_and_user/team.py
@@ -0,0 +1,16 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+from ._lazy import _useUser
+
+
+class Team(Schema):
+    members = fields.List(fields.Nested(_useUser))
+    name = fields.String()
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/examples/x01separated/team_and_user/user.py b/examples/x01separated/team_and_user/user.py
new file mode 100644
index 0000000..a2bd447
--- /dev/null
+++ b/examples/x01separated/team_and_user/user.py
@@ -0,0 +1,16 @@
+# this is auto-generated by swagger-marshmallow-codegen
+from __future__ import annotations
+from marshmallow import (
+    Schema,
+    fields,
+    INCLUDE,
+)
+from ._lazy import _useTeam
+
+
+class User(Schema):
+    name = fields.String()
+    team = fields.Nested(_useTeam)
+
+    class Meta:
+        unknown = INCLUDE
diff --git a/shapes/try.py b/shapes/try.py
index ba40395..3d68a0b 100644
--- a/shapes/try.py
+++ b/shapes/try.py
@@ -6,9 +6,9 @@ if t.TYPE_CHECKING:
 
 
 def setup(driver: Driver) -> None:
-    driver.options["targets"]["additional_properties_default"] = False
-    driver.options["targets"]["emit_schema_even_primitive_type"] = True
-    driver.options["targets"]["header_comment"] = ""
+    driver.config["additional_properties_default"] = False
+    driver.config["emit_schema_even_primitive_type"] = True
+    driver.config["header_comment"] = ""
 
 
 if __name__ == "__main__":
diff --git a/swagger_marshmallow_codegen/__main__.py b/swagger_marshmallow_codegen/__main__.py
index a9832ae..403e94e 100644
--- a/swagger_marshmallow_codegen/__main__.py
+++ b/swagger_marshmallow_codegen/__main__.py
@@ -1,3 +1,4 @@
 if __name__ == "__main__":
     from swagger_marshmallow_codegen.cmd import main
+
     main()
diff --git a/swagger_marshmallow_codegen/cmd.py b/swagger_marshmallow_codegen/cmd.py
index 345a98a..8a953fc 100644
--- a/swagger_marshmallow_codegen/cmd.py
+++ b/swagger_marshmallow_codegen/cmd.py
@@ -5,8 +5,10 @@ import logging
 import argparse
 from magicalimport import import_symbol
 
+
 if t.TYPE_CHECKING:
-    from swagger_marshmallow_codegen.driver import Driver
+    from swagger_marshmallow_codegen.driver import Driver, ConfigDict
+
 logger = logging.getLogger(__name__)
 
 
@@ -22,6 +24,9 @@ def main(setup: t.Optional[t.Callable[[Driver], None]] = None):
     parser.add_argument("--legacy", action="store_true")
     parser.add_argument("--strict-additional-properties", action="store_true")
     parser.add_argument("--explicit", action="store_true")
+
+    parser.add_argument("-d", "--separated-output", default=None)
+
     parser.add_argument("file", default=None)
     args = parser.parse_args()
 
@@ -37,20 +42,25 @@ def main(setup: t.Optional[t.Callable[[Driver], None]] = None):
     if ":" not in driver_cls:
         driver_cls = "swagger_marshmallow_codegen.driver:{}".format(driver_cls)
 
+    config: ConfigDict = {
+        "emit_schema": True,
+        "emit_input": False,
+        "emit_output": False,
+        "additional_properties_default": not args.strict_additional_properties,
+        "separated_output": bool(args.separated_output),
+    }
     if args.full:
-        config = {"targets": {"schema": True, "input": True, "output": True}}
-    else:
-        config = {"targets": {"schema": True}}
+        config["emit_input"] = True
+        config["emit_output"] = True
 
-    config["targets"][
-        "additional_properties_default"
-    ] = not args.strict_additional_properties
+    logger.debug("config is %r", config)
 
     driver = import_symbol(driver_cls, cwd=True)(config)
     if setup is not None:
         setup(driver)
+
     if args.file is None:
-        driver.run(sys.stdin, sys.stdout)
+        driver.run(sys.stdin, output=args.separated_output)
     else:
         with open(args.file) as rf:
-            driver.run(rf, sys.stdout)
+            driver.run(rf, output=args.separated_output)
diff --git a/swagger_marshmallow_codegen/codegen/__init__.py b/swagger_marshmallow_codegen/codegen/__init__.py
index 92bce96..8bcac14 100644
--- a/swagger_marshmallow_codegen/codegen/__init__.py
+++ b/swagger_marshmallow_codegen/codegen/__init__.py
@@ -2,12 +2,15 @@ from __future__ import annotations
 import typing as t
 import logging
 from functools import partial
-from .context import Context  # noqa:F401
-from .v2.codegen import SchemaWriter  # noqa:F401
 from .config import ConfigDict
+from .context import Context
+from .context import OneFileContextFactory, SeparatedFilesContextFactory
+from .v2.codegen import SchemaWriter  # noqa:F401
 
 if t.TYPE_CHECKING:
     from swagger_marshmallow_codegen.resolver import Resolver
+    from .context import InputData
+    from .context import OutputData
 
 logger = logging.getLogger(__name__)
 
@@ -40,23 +43,26 @@ class Codegen:
         self.schema_writer_factory = schema_writer_factory
 
     def codegen(
-        self,
-        d,
-        config: ConfigDict,
-        *,
-        ctx: t.Optional[Context] = None,
-        test: bool = False,
-    ):
-        if test:
-            # todo: use dataclasses?
-            config["skip_header_comment"] = True
-
-        cls = self.guess_factory(d, config=config, path=self.version_path)
-        codegen = cls(
+        self, d: InputData, config: ConfigDict, *, ctx: t.Optional[Context] = None,
+    ) -> OutputData:
+        codegen_cls = self.guess_factory(d, config=config, path=self.version_path)
+        codegen = codegen_cls(
             schema_class_path=self.schema_class_path,
             schema_writer_factory=self.schema_writer_factory,
         )
-        return codegen.codegen(d, ctx=ctx)
+        if config.get("separated_output", False):
+            context_factory_cls = SeparatedFilesContextFactory
+        else:
+            context_factory_cls = OneFileContextFactory
+
+        context_factory = context_factory_cls(
+            ctx or Context(), setup=codegen.setup_context
+        )
+        try:
+            return codegen.codegen(d, context_factory=context_factory)
+        finally:
+            if context_factory.teardown is not None:
+                context_factory.teardown(codegen.accessor.resolver)
 
     def guess_factory(
         self, d: t.Dict, *, config: ConfigDict, path: str
diff --git a/swagger_marshmallow_codegen/codegen/config.py b/swagger_marshmallow_codegen/codegen/config.py
index cce04ff..66c20fd 100644
--- a/swagger_marshmallow_codegen/codegen/config.py
+++ b/swagger_marshmallow_codegen/codegen/config.py
@@ -3,13 +3,14 @@ import typing_extensions as tx
 
 
 class ConfigDict(tx.TypedDict, total=False):
-    schema: bool  # emit definitions schemas
-    input: bool  # emit input schema
-    output: bool  # emit output schema
+    emit_schema: bool  # emit definitions schemas
+    emit_input: bool  # emit input schema
+    emit_output: bool  # emit output schema
+
+    separated_output: bool  # activate separated output
 
     explicit: bool  # emit Meta.unknown=RAISE, explicitly
     additional_properties_default: bool  # if true meta.unknown=INCLUDE
 
     emit_schema_even_primitive_type: bool  # emit not used toplevel definitions
-    skip_header_comment: bool
-    header_comment: str
+    header_comment: str  # header comment
diff --git a/swagger_marshmallow_codegen/codegen/context.py b/swagger_marshmallow_codegen/codegen/context.py
index 1e835be..0312e02 100644
--- a/swagger_marshmallow_codegen/codegen/context.py
+++ b/swagger_marshmallow_codegen/codegen/context.py
@@ -1,21 +1,158 @@
+from __future__ import annotations
 import logging
+import typing as t
+import typing_extensions as tx
 from prestring.python import Module, Symbol, FromStatement
 
+InputData = t.Dict[str, t.Any]
+if t.TYPE_CHECKING:
+    from swagger_marshmallow_codegen.resolver import Resolver
 logger = logging.getLogger(__name__)
 
 
 class Context:
-    def __init__(self, m=None, im=None):
+    def __init__(
+        self,
+        *,
+        name: str = "",
+        m: t.Optional[Module] = None,
+        im: t.Optional[Module] = None,
+        rim: t.Optional[Module] = None,
+        relative_imported: t.Optional[t.Dict[str, Symbol]] = None,
+        separated: bool = False,
+    ):
+        self.name = name
+        self.separated = separated
+
         self.m: Module = m or Module()
         self.im: Module = im or self.m.submodule()
+        self.rim: Module = rim or self.m.submodule()
+
+        self._relative_imported = relative_imported
+        if relative_imported is None:
+            self._relative_imported = {}
 
-    def from_(self, module, name) -> FromStatement:
+    def from_(self, module: str, name: str) -> FromStatement:
         logger.debug("      import: module=%s, name=%s", module, name)
         return self.im.from_(module, name)
 
-    def import_(self, module) -> Symbol:
+    def import_(self, module: str) -> Symbol:
         logger.debug("      import: module=%s", module)
         return self.im.import_(module)
 
-    def new_child(self):
-        return self.__class__(self.m.submodule(newline=False), self.im)
+    def relative_import_from_lazy(self, name: str) -> None:
+        if not self.separated:
+            return
+
+        imported = self._relative_imported.get(name)
+        if imported is not None:
+            return None
+        logger.debug("      relative import lazy: module=.%s	symbol:%s", name, name)
+        self._relative_imported[name] = self.rim.from_("._lazy", f"_use{name}")
+        return None
+
+    def relative_import(self, name: str) -> None:
+        if not self.separated:
+            return
+        logger.debug("      relative import: module=.%s	symbol:%s", name, name)
+        self.rim.from_(f".{name}", name)
+        return None
+
+    def use_relative(self, name: str) -> t.Any:
+        if not self.separated:
+            return f"lambda: {name}()"
+        elif self.name == name:
+            # self recursion
+            return f"lambda: {name}()"
+        elif name not in self._relative_imported:
+            # inline definition
+            return f"lambda: {name}()"
+        else:
+            return f"_use{name}"
+
+    def new_child(self) -> Context:
+        return self.__class__(
+            name=self.name,
+            m=self.m.submodule(newline=False),
+            im=self.im,
+            rim=self.rim,
+            relative_imported=self._relative_imported,
+            separated=self.separated,
+        )
+
+
+@tx.runtime_checkable
+class ContextFactory(tx.Protocol):
+    setup: t.Optional[t.Callable[[Context], None]]
+    teardown: t.Optional[t.Callable[[Resolver], None]]
+
+    def __call__(self, name: str, *, part: t.Optional[str] = None) -> Context:
+        ...
+
+
+@tx.runtime_checkable
+class OutputData(tx.Protocol):
+    @property
+    def files(self) -> t.Iterator[t.Tuple[str, t.Any]]:
+        ...
+
+
+class SeparatedFilesContextFactory:
+    def __init__(self, ctx: Context, *, setup: t.Callable[[Context], None]) -> None:
+        self._files: t.Dict[str, Context] = {}
+        self._parts: t.Dict[t.Tuple[str, t.Optional[str]], Context] = {}
+        self._root = ctx
+
+        self.setup = setup
+        self.teardown = self._teardown
+
+    def __call__(self, name: str, *, part: t.Optional[str] = None) -> Context:
+        ctx = self._files.get(name)
+        if ctx is None:
+            ctx = self._files[name] = Context(name=name, separated=True)
+            ctx._relative_imported[name] = Symbol(name)
+            self.setup(ctx)
+
+        sctx = self._parts.get((name, part))
+        if sctx is None:
+            sctx = self._parts[(name, part)] = ctx.new_child()
+        return sctx
+
+    @property
+    def files(self) -> t.Iterator[t.Tuple[str, t.Any]]:
+        return sorted([(name, ctx.m) for name, ctx in self._files.items()])
+
+    def _teardown(self, resolver: Resolver) -> None:
+        names = [name for name, _ in self.files]
+
+        c = self._files["__init__"] = Context(name="__init__", separated=True)
+        for name in names:
+            c.from_(f".{name}", resolver.resolve_schema_name(name))
+
+        c = self._files["_lazy"] = Context(name="_lazy", separated=True)
+        for name in names:
+            cls_name = resolver.resolve_schema_name(name)
+            with c.m.def_(f"_use{cls_name}"):
+                c.m.from_(f".{name}", cls_name)
+                c.m.return_(cls_name)
+
+
+class OneFileContextFactory:
+    def __init__(self, ctx: Context, *, setup: t.Callable[[Context], None]) -> None:
+        self._parts: t.Dict[t.Optional[str], Context] = {}
+        self._root = ctx
+
+        self.setup = setup
+        self.setup(ctx)
+        self.teardown = None
+
+    def __call__(self, name: str, *, part: t.Optional[str] = None) -> Context:
+        ctx = self._parts.get(part)
+        if ctx is not None:
+            return ctx
+        ctx = self._parts[part] = self._root.new_child()
+        return ctx
+
+    @property
+    def files(self) -> t.Iterator[t.Tuple[str, t.Any]]:
+        yield ("", self._root.m)
diff --git a/swagger_marshmallow_codegen/codegen/v2/accessor.py b/swagger_marshmallow_codegen/codegen/v2/accessor.py
index 5aea116..c7f9563 100644
--- a/swagger_marshmallow_codegen/codegen/v2/accessor.py
+++ b/swagger_marshmallow_codegen/codegen/v2/accessor.py
@@ -4,55 +4,65 @@ import logging
 
 if t.TYPE_CHECKING:
     from swagger_marshmallow_codegen.resolver import Resolver
-    from swagger_marshmallow_codegen.codegen.config import ConfigDict
+    from swagger_marshmallow_codegen.dispatcher import FormatDispatcher
+    from ..config import ConfigDict
+    from ..context import Context
 logger = logging.getLogger(__name__)
 
 
 class Accessor:
-    def __init__(self, resolver: Resolver, *, config: ConfigDict):
+    def __init__(self, resolver: Resolver, *, config: ConfigDict) -> None:
         self.resolver = resolver
         self.config = config
 
     @property
-    def dispatcher(self):
+    def dispatcher(self) -> FormatDispatcher:
         return self.resolver.dispatcher
 
-    def paths(self, d):
+    def paths(self, d: t.Dict[str, t.Any]) -> t.List[t.Dict[str, t.Any]]:
         return (d.get("paths") or {}).items()
 
     def methods(
         self,
-        d,
-        candidates=set(("get", "post", "put", "head", "delete", "options", "patch")),
-    ):
+        d: t.Dict[str, t.Any],
+        candidates: t.Set[str] = set(
+            ("get", "post", "put", "head", "delete", "options", "patch")
+        ),
+    ) -> t.Iterator[t.Tuple[str, t.Dict[str, t.Any]]]:
         for method, definition in d.items():
             if method in candidates:
                 yield method, definition
 
-    def parameters(self, d):
+    def parameters(self, d: t.Dict[str, t.Any]) -> t.List[t.Dict[str, t.Any]]:
         return d.get("parameters") or []
 
-    def responses(self, d):
+    def responses(
+        self, d: t.Dict[str, t.Any]
+    ) -> t.Iterator[t.Tuple[str, t.Dict[str, t.Any]]]:
         for name, definition in (d.get("responses") or {}).items():
             name = str(name)
             if not name.startswith("x-"):
                 yield name, definition
 
-    def definitions(self, d):
+    def definitions(self, d: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
         return (d.get("definitions") or {}).items()
 
-    def properties(self, d):
+    def properties(self, d: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
         return d.get("properties") or {}
 
-    def pattern_properties(self, d):
+    def pattern_properties(self, d: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
         return d.get("patternProperties") or {}
 
-    def update_options_pre_properties(self, d, opts):
+    def update_options_pre_properties(
+        self, d: t.Dict[str, t.Any], opts: t.Dict[str, t.Any]
+    ) -> t.Dict[str, t.Any]:
         for name in d.get("required") or []:
             opts[name]["required"] = True
         return opts
 
-    def update_option_on_property(self, c, field, opts):
+    def update_option_on_property(
+        self, c: Context, field: t.Dict[str, t.Any], opts: t.Dict[str, t.Any]
+    ) -> t.Dict[str, t.Any]:
         if "description" in field:
             opts["description"] = field["description"]
         if self.resolver.has_many(field):
diff --git a/swagger_marshmallow_codegen/codegen/v2/codegen.py b/swagger_marshmallow_codegen/codegen/v2/codegen.py
index 188cc46..6a9d71c 100644
--- a/swagger_marshmallow_codegen/codegen/v2/codegen.py
+++ b/swagger_marshmallow_codegen/codegen/v2/codegen.py
@@ -3,27 +3,34 @@ import typing as t
 import keyword
 import logging
 from collections import namedtuple
+from collections import defaultdict
+from collections import OrderedDict
 from functools import partial
 from prestring import PreString
+from prestring.python import Module
 from prestring.utils import LazyFormat, LazyArgumentsAndKeywords, LazyKeywords
 from dictknife import deepmerge
-from collections import defaultdict
-from collections import OrderedDict
 from swagger_marshmallow_codegen.langhelpers import (
     LazyCallString,
     titleize,
     clsname_from_path,
 )
+from swagger_marshmallow_codegen.constants import (
+    X_MARSHMALLOW_NAME,
+    X_MARSHMALLOW_INLINE,
+)
 from ..context import Context
 from ..config import ConfigDict
 from .accessor import Accessor
 
 if t.TYPE_CHECKING:
     from swagger_marshmallow_codegen.resolver import Resolver
-
+    from ..context import ContextFactory, InputData, OutputData
 
 logger = logging.getLogger(__name__)
-NAME_MARKER = "x-marshmallow-name"
+NAME_MARKER = X_MARSHMALLOW_NAME
+
+PathInfo = namedtuple("PathInfo", "info, required")
 
 
 class CodegenError(Exception):
@@ -35,7 +42,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
@@ -68,11 +75,11 @@ class SchemaWriter:
             field = field["items"]
             if self.resolver.has_ref(field):
                 field_class_name, field = self.resolver.resolve_ref_definition(
-                    d, field, level=1
+                    c, d, field, level=1
                 )
                 # finding original definition
                 if self.resolver.has_ref(field):
-                    ref_name, field = self.resolver.resolve_ref_definition(d, field)
+                    ref_name, field = self.resolver.resolve_ref_definition(c, d, field)
                     if ref_name is None:
                         raise CodegenError("ref: %r is not found", field["$ref"])
 
@@ -108,9 +115,7 @@ class SchemaWriter:
             if caller_name == "fields.Nested":
                 return LazyFormat(
                     "fields.Nested({})",
-                    LazyArgumentsAndKeywords(
-                        [LazyFormat("lambda: {}()", field_class_name)], opts,
-                    ),
+                    LazyArgumentsAndKeywords([c.use_relative(field_class_name)], opts,),
                 )
             else:
                 return LazyFormat("{}({})", caller_name, LazyKeywords(opts))
@@ -120,9 +125,7 @@ class SchemaWriter:
             opts = {k: repr(v) for k, v in opts.items()}
             return LazyFormat(
                 "fields.Nested({})",
-                LazyArgumentsAndKeywords(
-                    [LazyFormat("lambda: {}()", field_class_name)], opts,
-                ),
+                LazyArgumentsAndKeywords([c.use_relative(field_class_name)], opts,),
             )
         elif caller_name == "fields.Dict":
             self.accessor.update_option_on_property(c, field, opts)
@@ -138,11 +141,11 @@ class SchemaWriter:
 
             if self.resolver.has_ref(field):
                 field_class_name, field = self.resolver.resolve_ref_definition(
-                    d, field, level=1
+                    c, d, field, level=1
                 )
                 # finding original definition
                 if self.resolver.has_ref(field):
-                    ref_name, field = self.resolver.resolve_ref_definition(d, field)
+                    ref_name, field = self.resolver.resolve_ref_definition(c, d, field)
                     if ref_name is None:
                         raise CodegenError("ref: %r is not found", field["$ref"])
 
@@ -185,7 +188,7 @@ class SchemaWriter:
         field_class_name = None
         if self.resolver.has_ref(field):
             field_class_name, field = self.resolver.resolve_ref_definition(
-                d, field, level=1
+                c, d, field, level=1
             )
             if self.resolver.has_many(field):
                 return self.write_field_one(
@@ -194,7 +197,7 @@ class SchemaWriter:
 
             # finding original definition
             if self.resolver.has_ref(field):
-                ref_name, field = self.resolver.resolve_ref_definition(d, field)
+                ref_name, field = self.resolver.resolve_ref_definition(c, d, field)
                 if self.resolver.has_many(field):
                     return self.write_field_one(
                         c, d, field_class_name, definition, name, field, opts, many=True
@@ -248,7 +251,7 @@ class SchemaWriter:
         base_classes = [self.schema_class]
         if self.resolver.has_ref(definition):
             ref_name, ref_definition = self.resolver.resolve_ref_definition(
-                d, definition
+                c, d, definition
             )
             if ref_name is None:
                 raise CodegenError("$ref %r is not found", definition["$ref"])
@@ -258,7 +261,7 @@ class SchemaWriter:
                 items = ref_definition["items"]
                 if self.resolver.has_ref(items):
                     _, items = self.resolver.resolve_ref_definition(
-                        d, ref_definition["items"]
+                        c, d, ref_definition["items"]
                     )
                 if not self.resolver.has_schema(d, items):
                     return self.write_primitive_schema(
@@ -276,12 +279,13 @@ class SchemaWriter:
                 base_classes = [ref_name]
         elif self.resolver.has_allof(definition):
             ref_list, ref_definition = self.resolver.resolve_allof_definition(
-                d, definition
+                c, d, definition
             )
             definition = deepmerge(ref_definition, definition)
             if ref_list:
                 base_classes = []
                 for ref_name, ref_definition in ref_list:
+                    c.relative_import(ref_name)
                     if ref_name is None:
                         raise CodegenError(
                             "$ref %r is not found", ref_definition
@@ -309,7 +313,7 @@ class SchemaWriter:
                 return self.write_primitive_schema(c, d, clsname, definition, many=many)
             else:
                 ref_name, ref_definition = self.resolver.resolve_ref_definition(
-                    d, definition["items"]
+                    c, d, definition["items"]
                 )
                 if ref_name is None:
                     return self.write_primitive_schema(
@@ -360,7 +364,7 @@ class SchemaWriter:
                 subdef = definition["additionalProperties"]
                 with c.m.class_("Meta"):
                     if self.resolver.has_ref(subdef):
-                        ref_name, _ = self.resolver.resolve_ref_definition(d, subdef)
+                        ref_name, _ = self.resolver.resolve_ref_definition(c, d, subdef)
                         if ref_name is None:
                             raise CodegenError("$ref %r is not found", subdef["$ref"])
                         self.write_field_one(
@@ -425,7 +429,8 @@ class DefinitionsSchemaWriter:
     def config(self) -> ConfigDict:
         return self.accessor.config
 
-    def write(self, c, d):
+    def write(self, d: InputData, *, context_factory: ContextFactory) -> None:
+        part = self.__class__.__name__
         for schema_name, definition in self.accessor.definitions(d):
             if not self.config.get(
                 "emit_schema_even_primitive_type", False
@@ -433,6 +438,9 @@ class DefinitionsSchemaWriter:
                 logger.info("write schema: skip %s", schema_name)
                 continue
 
+            c = context_factory(
+                definition.get(X_MARSHMALLOW_INLINE) or schema_name, part=part,
+            )
             clsname = self.resolver.resolve_schema_name(schema_name)
             logger.info("write schema: write %s", schema_name)
             self.schema_writer.write_schema(c, d, clsname, definition)
@@ -452,9 +460,10 @@ class PathsSchemaWriter:
     def get_lazy_clsname(self, path):
         return PreString(clsname_from_path(path))
 
-    def write(self, c, d):
+    def write(self, d: InputData, *, context_factory: ContextFactory) -> None:
+        part = self.__class__.__name__
         for path, methods in self.accessor.paths(d):
-            sc = c.new_child()
+            sc = context_factory(path, part=part)
             found = False
             lazy_clsname = self.get_lazy_clsname(path)
             with sc.m.class_(LazyFormat("{}Input", lazy_clsname)):
@@ -481,7 +490,10 @@ class PathsSchemaWriter:
                             ssc.m.stmt("")
 
                         path_info = self.build_path_info(
-                            d, toplevel_parameters, self.accessor.parameters(definition)
+                            sc,
+                            d,
+                            toplevel_parameters,
+                            self.accessor.parameters(definition),
                         )
                         for section, properties in sorted(path_info.info.items()):
                             if section is None:
@@ -509,21 +521,24 @@ class PathsSchemaWriter:
             if not found:
                 sc.m.clear()
 
-    PathInfo = namedtuple("PathInfo", "info, required")
-
-    def build_path_info(self, fulldata, *paramaters_set):
+    def build_path_info(
+        self,
+        c: Context,
+        fulldata: t.Dict[str, t.Any],
+        *paramaters_set: t.List[t.Dict[str, t.Any]]
+    ) -> PathInfo:
         info = defaultdict(OrderedDict)
         required = defaultdict(list)
         for parameters in paramaters_set:
             for p in parameters:
                 if self.resolver.has_ref(p):
-                    _, p = self.resolver.resolve_ref_definition(fulldata, p)
+                    _, p = self.resolver.resolve_ref_definition(c, fulldata, p)
                 name = p.get("name")
                 section = p.get("in")
                 info[section][name] = p
                 if p.get("required"):
                     required[section].append(name)
-        return self.PathInfo(info=info, required=required)
+        return PathInfo(info=info, required=required)
 
 
 class ResponsesSchemaWriter:
@@ -541,10 +556,11 @@ class ResponsesSchemaWriter:
     def get_lazy_clsname(self, path):
         return PreString(clsname_from_path(path))
 
-    def write(self, c, d):
+    def write(self, d: InputData, *, context_factory: ContextFactory) -> None:
+        part = self.__class__.__name__
         for path, methods in self.accessor.paths(d):
             lazy_clsname = self.get_lazy_clsname(path)
-            sc = c.new_child()
+            sc = context_factory(path, part=part)
             found = False
             with sc.m.class_(LazyFormat("{}Output", lazy_clsname)):
                 if self.OVERRIDE_NAME_MARKER in methods:
@@ -554,7 +570,7 @@ class ResponsesSchemaWriter:
                     for status, definition in self.accessor.responses(definition):
                         if self.resolver.has_ref(definition):
                             _, definition = self.resolver.resolve_ref_definition(
-                                d, definition
+                                sc, d, definition
                             )
                         if "schema" in definition:
                             found = True
@@ -589,41 +605,54 @@ class Codegen:
         )
         self.schema_class = self.schema_class_path.rsplit(":", 1)[-1]
 
+        self.files: t.Dict[str, Module] = {}
+
     @property
     def resolver(self) -> Resolver:
         return self.accessor.resolver
 
-    def write_header(self, c, *, comment: t.Optional[str] = None):
+    def write_header(self, c: Context, *, comment: t.Optional[str] = None) -> None:
         if comment is None:
             comment = """\
 # this is auto-generated by swagger-marshmallow-codegen
 from __future__ import annotations
 """
+        if not comment:
+            return
+
         for line in comment.splitlines():
             c.im.stmt(line)
 
-    def write_import_(self, c):
+    def write_import_(self, c: Context) -> None:
         c.from_(*self.schema_class_path.rsplit(":", 1))
         c.from_("marshmallow", "fields")
 
-    def write_body(self, c, d):
+    def write_body(self, d: InputData, *, context_factory: ContextFactory) -> None:
+        # TODO: get from context
         sw = self.schema_writer_factory(self.accessor, self.schema_class)
+
         config = self.accessor.config
-        if config.get("schema", False):
-            DefinitionsSchemaWriter(self.accessor, sw).write(c.new_child(), d)
-        if config.get("input", False):
-            PathsSchemaWriter(self.accessor, sw).write(c.new_child(), d)
-        if config.get("output", False):
-            ResponsesSchemaWriter(self.accessor, sw).write(c.new_child(), d)
-
-    def codegen(self, d, ctx=None):
-        c = ctx or Context()
-        if not self.accessor.config.get("skip_header_comment", False):
-            self.write_header(c, comment=self.accessor.config.get("header_comment"))
-        c.m.sep()
-        self.write_import_(c)
-        self.write_body(c, d)
-        return c.m
+        if config.get("emit_schema", False):
+            DefinitionsSchemaWriter(self.accessor, sw).write(
+                d, context_factory=context_factory
+            )
+        if config.get("emit_input", False):
+            PathsSchemaWriter(self.accessor, sw).write(
+                d, context_factory=context_factory
+            )
+        if config.get("emit_output", False):
+            ResponsesSchemaWriter(self.accessor, sw).write(
+                d, context_factory=context_factory
+            )
+
+    def setup_context(self, ctx: Context) -> None:
+        self.write_header(ctx, comment=self.accessor.config.get("header_comment"))
+        self.write_import_(ctx)
+        ctx.m.sep()
+
+    def codegen(self, d: InputData, context_factory) -> OutputData:
+        self.write_body(d, context_factory=context_factory)
+        return context_factory
 
 
 def lazy_json_dump(s):
diff --git a/swagger_marshmallow_codegen/codegen/v3/codegen.py b/swagger_marshmallow_codegen/codegen/v3/codegen.py
index 0e27d70..01e4d58 100644
--- a/swagger_marshmallow_codegen/codegen/v3/codegen.py
+++ b/swagger_marshmallow_codegen/codegen/v3/codegen.py
@@ -1,2 +1,3 @@
 from ..v2.codegen import Codegen
+
 __all__ = ["Codegen"]
diff --git a/swagger_marshmallow_codegen/constants.py b/swagger_marshmallow_codegen/constants.py
new file mode 100644
index 0000000..1c0771c
--- /dev/null
+++ b/swagger_marshmallow_codegen/constants.py
@@ -0,0 +1,2 @@
+X_MARSHMALLOW_NAME = "x-marshmallow-name"
+X_MARSHMALLOW_INLINE = "x-marshmallow-inline"
diff --git a/swagger_marshmallow_codegen/dispatcher.py b/swagger_marshmallow_codegen/dispatcher.py
index 2bf21f1..dd913a4 100644
--- a/swagger_marshmallow_codegen/dispatcher.py
+++ b/swagger_marshmallow_codegen/dispatcher.py
@@ -1,4 +1,4 @@
-# -*- coding:utf-8 -*-
+from __future__ import annotations
 import logging
 from functools import partial
 from collections import namedtuple
diff --git a/swagger_marshmallow_codegen/driver.py b/swagger_marshmallow_codegen/driver.py
index fd46fbf..24c8531 100644
--- a/swagger_marshmallow_codegen/driver.py
+++ b/swagger_marshmallow_codegen/driver.py
@@ -1,10 +1,17 @@
-# -*- coding:utf-8 -*-
+from __future__ import annotations
 import logging
+import typing as t
+from prestring.output import output as create_separated_output
 from . import loading
 from .resolver import Resolver
 from .codegen import Codegen, SchemaWriter
 from .dispatcher import FormatDispatcher
 from .lifting import lifting_definition
+from .langhelpers import normalize
+
+if t.TYPE_CHECKING:
+    from .codegen.config import ConfigDict
+    from .codegen.context import InputData, OutputData
 
 logger = logging.getLogger(__name__)
 
@@ -14,26 +21,36 @@ class Driver:
     resolver_factory = Resolver
     codegen_factory = Codegen
 
-    def __init__(self, options):
-        self.options = options
+    def __init__(self, config: ConfigDict):
+        self.config: ConfigDict = config
 
-    def load(self, fp):
+    def load(self, fp: t.Any) -> InputData:
         # hack:
         # import dictknife.loading._yaml as xxx
         # xxx.make_dict = dict
         return loading.load(fp)
 
-    def dump(self, m, fp):
-        return print(m, file=fp)
+    def dump(self, d: OutputData, *, output: t.Optional[str] = None) -> None:
+        if output is None:
+            for name, m in d.files:
+                print(m)
+            return
+
+        with create_separated_output(normalize(output), suffix=".py") as fs:
+            seen = set()
+            for name, m in d.files:
+                seen.add(name)
+                with fs.open(name, "w") as wf:
+                    print(m, file=wf)
 
-    def transform(self, d):
+    def transform(self, d: InputData) -> OutputData:
         d = lifting_definition(d)
-        return self.create_codegen().codegen(d, config=self.options["targets"])
+        return self.create_codegen().codegen(d, config=self.config)
 
-    def run(self, inp, outp):
+    def run(self, inp, *, output: t.Optional[str] = None):
         data = self.load(inp)
         result = self.transform(data)
-        self.dump(result, outp)
+        self.dump(result, output=output)
 
     def create_codegen(self):
         dispatcher = self.dispatcher_factory()
@@ -53,26 +70,34 @@ class LegacyDriver(Driver):
 
 
 class Flatten:
-    def __init__(self, options):
-        self.options = options
+    def __init__(self, config: ConfigDict) -> None:
+        self.config: ConfigDict = config
 
-    def load(self, fp):
+    def load(self, fp: t.Any) -> InputData:
         return loading.load(fp)
 
-    def dump(self, d, fp):
-        return loading.dump(d, fp)
+    def dump(self, result: OutputData, *, output: t.Optional[str] = None) -> None:
+        for _, d in result.files:
+            return loading.dump(d, output)
+
+    def transform(self, d: InputData) -> OutputData:
+        lifted = lifting_definition(d)
+
+        class _Data:
+            @property
+            def files(self) -> t.Iterator[t.Tuple[str, t.Any]]:
+                yield lifted
 
-    def transform(self, d):
-        return lifting_definition(d)
+        return _Data()
 
-    def run(self, inp, outp):
+    def run(self, inp, *, output: t.Optional[str] = None):
         data = self.load(inp)
         result = self.transform(data)
-        self.dump(result, outp)
+        self.dump(result, output=output)
 
 
 class ProfileDriver(Driver):
-    def run(self, inp, outp):
+    def run(self, inp, *, output: t.Optional[str] = None):
         import cProfile
         import pstats
 
@@ -83,4 +108,4 @@ class ProfileDriver(Driver):
         profile.disable()
         s = pstats.Stats(profile)
         s.dump_stats("swagger-marshmallow-codegen.prof")
-        self.dump(result, outp)
+        self.dump(result, output=output)
diff --git a/swagger_marshmallow_codegen/lifting.py b/swagger_marshmallow_codegen/lifting.py
index e002dea..cb59b51 100644
--- a/swagger_marshmallow_codegen/lifting.py
+++ b/swagger_marshmallow_codegen/lifting.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 import copy
 from dictknife.jsonknife.lifting import Handler
 from dictknife.accessing import Accessor
+from swagger_marshmallow_codegen.constants import X_MARSHMALLOW_INLINE
 from .langhelpers import titleize
 
 
@@ -52,7 +53,11 @@ class Flattener:
         if "$ref" in data:
             return data
         fullname = ctx.full_name()
+
+        if ctx.path[0] != fullname:
+            data[X_MARSHMALLOW_INLINE] = ctx.path[0]
         ctx.save_object(fullname, data)
+
         return self.return_definition(data, fullname, typ="object")
 
     def on_array_has_items(self, data, ctx):
@@ -61,7 +66,11 @@ class Flattener:
         fullname = ctx.full_name()
         ctx.add_array_item()
         data["items"] = self._extract(data["items"], ctx, from_array=True)
+
+        if ctx.path[0] != fullname:
+            data[X_MARSHMALLOW_INLINE] = ctx.path[0]
         ctx.save_array(fullname, data)
+
         ctx.pop_name()
         return self.return_definition(data, fullname, typ="array")
 
diff --git a/swagger_marshmallow_codegen/resolver.py b/swagger_marshmallow_codegen/resolver.py
index 2c47434..52b2319 100644
--- a/swagger_marshmallow_codegen/resolver.py
+++ b/swagger_marshmallow_codegen/resolver.py
@@ -1,26 +1,32 @@
-# -*- coding:utf-8 -*-
+from __future__ import annotations
+import typing as t
 import logging
 from collections import OrderedDict
 import dictknife
+from .constants import X_MARSHMALLOW_INLINE
 from .langhelpers import titleize, normalize
 from .dispatcher import Pair
 from . import validate
 
+if t.TYPE_CHECKING:
+    from .dispatcher import FormatDispatcher
+    from .codegen.context import Context
+
 logger = logging.getLogger(__name__)
 
 
 class Resolver:
-    def __init__(self, dispatcher):
+    def __init__(self, dispatcher: FormatDispatcher) -> None:
         self.dispatcher = dispatcher
         self.accessor = dictknife.Accessor()  # todo: rename
 
-    def has_ref(self, d):
+    def has_ref(self, d) -> bool:
         return "$ref" in d
 
-    def has_allof(self, d):
+    def has_allof(self, d) -> bool:
         return "allOf" in d
 
-    def has_schema(self, fulldata, d, cand=("object",), fullscan=True):
+    def has_schema(self, fulldata, d, cand=("object",), fullscan=True) -> bool:
         typ = d.get("type", None)
         if typ in cand:
             return True
@@ -32,24 +38,24 @@ class Resolver:
             return False
         if not fullscan:
             return False
-        _, definition = self.resolve_ref_definition(fulldata, d)
+        _, definition = self.resolve_ref_definition(None, fulldata, d)
         return self.has_schema(fulldata, definition, fullscan=False)
 
-    def has_nested(self, fulldata, d):
+    def has_nested(self, fulldata, d) -> bool:
         if self.has_schema(fulldata, d, fullscan=False):
             return True
         return self.has_many(d) and self.has_schema(fulldata, d["items"])
 
-    def has_many(self, d):
+    def has_many(self, d) -> bool:
         return d.get("type") == "array" or "items" in d
 
-    def resolve_normalized_name(self, name):
+    def resolve_normalized_name(self, name: str) -> str:
         return normalize(name)
 
-    def resolve_schema_name(self, name):
+    def resolve_schema_name(self, name: str) -> str:
         return titleize(name)
 
-    def resolve_type_and_format(self, name, field):
+    def resolve_type_and_format(self, name: str, field: t.Dict[str, t.Any]) -> Pair:
         try:
             typ = field["type"]
             if isinstance(typ, (list, tuple)):
@@ -67,7 +73,9 @@ class Resolver:
                 return Pair(type="any", format=None)
             return Pair(type="object", format=None)
 
-    def resolve_caller_name(self, c, field_name, field):
+    def resolve_caller_name(
+        self, c: Context, field_name: str, field: t.Dict[str, t.Any]
+    ) -> t.Optional[str]:
         if "additionalProperties" in field and "properties" not in field:
             path = "marshmallow.fields:Dict"
         else:
@@ -86,24 +94,34 @@ class Resolver:
         logger.debug("    resolve: field=%s", caller_name)
         return caller_name
 
-    def resolve_allof_definition(self, fulldata, d):
+    def resolve_allof_definition(
+        self, c: Context, fulldata: t.Dict[str, t.Any], d: t.Dict[str, t.Any]
+    ) -> t.Tuple[t.List[t.Dict[str, t.Any]], t.Dict[str, t.Any]]:
         ref_list = []
         r = OrderedDict()
         for subdef in d["allOf"]:
             if self.has_ref(subdef):
-                ref_list.append(self.resolve_ref_definition(fulldata, subdef))
+                ref_list.append(self.resolve_ref_definition(c, fulldata, subdef))
             else:
                 r = dictknife.deepmerge(r, subdef)
         return ref_list, r
 
-    def resolve_ref_definition(self, fulldata, d, name=None, i=0, level=-1):
+    def resolve_ref_definition(
+        self,
+        c: t.Optional[Context],  # xxx
+        fulldata: t.Dict[str, t.Any],
+        d: t.Dict[str, t.Any],
+        name: t.Optional[str] = None,
+        i: int = 0,
+        level: int = -1,
+    ) -> t.Tuple[str, t.Dict[str, t.Any]]:
         # return schema_name, definition_dict
         # todo: support quoted "/"
         # on array
         if "items" in d:
             definition = d
             name, _ = self.resolve_ref_definition(
-                fulldata, d["items"], name=name, i=i, level=level + 1
+                c, fulldata, d["items"], name=name, i=i, level=level + 1
             )  # xxx
             return name, definition
 
@@ -121,11 +139,29 @@ class Resolver:
         if parent is None:
             logger.warning("%r is not found", d["$ref"])
             return self.resolve_schema_name(name), d
-        return self.resolve_ref_definition(
-            fulldata, parent[name], name=name, i=i + 1, level=level - 1
+        ref_name, definition = self.resolve_ref_definition(
+            c, fulldata, parent[name], name=name, i=i + 1, level=level - 1
         )
 
-    def resolve_validators_on_property(self, c, field):
+        # import for separated output
+        if X_MARSHMALLOW_INLINE not in definition:
+            if c is not None and (
+                "properties" in definition
+                or (
+                    (
+                        "additionalProperties" in definition
+                        or "items" in definition
+                        or "allOf" in definition
+                    )
+                    and self.has_schema(fulldata, definition)
+                )
+            ):
+                c.relative_import_from_lazy(ref_name)
+        return ref_name, definition
+
+    def resolve_validators_on_property(
+        self, c: Context, field: t.Dict[str, t.Any]
+    ) -> t.List[t.Any]:
         validators = []
 
         def add(validator):


Test patch:
diff --git a/swagger_marshmallow_codegen/tests/codegen_candidates.py b/swagger_marshmallow_codegen/tests/codegen_candidates.py
index 3a635c9..369df71 100644
--- a/swagger_marshmallow_codegen/tests/codegen_candidates.py
+++ b/swagger_marshmallow_codegen/tests/codegen_candidates.py
@@ -1,10 +1,22 @@
 CANDIDATES = [
-    ('simple-object', '00simple-object.json', '00simple-object.py'),
-    ('simple-primitive', '01simple-primitive.json', '01simple-primitive.py'),
-    ('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'),
-    ('additionalProperties-with-bool', '07additionalProperties-with-bool.json', '07additionalProperties-with-bool.py'),
+    ("simple-object", "00simple-object.json", "00simple-object.py"),
+    ("simple-primitive", "01simple-primitive.json", "01simple-primitive.py"),
+    ("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",
+    ),
+    (
+        "additionalProperties-with-bool",
+        "07additionalProperties-with-bool.json",
+        "07additionalProperties-with-bool.py",
+    ),
 ]
diff --git a/swagger_marshmallow_codegen/tests/legacy_src/00commit.yaml b/swagger_marshmallow_codegen/tests/legacy_src/00commit.yaml
index 8b308a3..d292167 100644
--- a/swagger_marshmallow_codegen/tests/legacy_src/00commit.yaml
+++ b/swagger_marshmallow_codegen/tests/legacy_src/00commit.yaml
@@ -9,10 +9,12 @@ definitions:
         $ref: '#/definitions/commitFiles'
     type: object
   commitFiles:
+    x-marshmallow-inline: commit
     type: array
     items:
       $ref: '#/definitions/commitFilesItem'
   commitFilesItem:
+    x-marshmallow-inline: commit
     type: object
     properties:
       additions:
@@ -32,6 +34,7 @@ definitions:
       status:
         type: string
   commitCommit:
+    x-marshmallow-inline: commit
     type: object
     properties:
       author:
@@ -45,6 +48,7 @@ definitions:
       url:
         type: string
   commitCommitTree:
+    x-marshmallow-inline: commit
     type: object
     properties:
       sha:
@@ -52,6 +56,7 @@ definitions:
       url:
         type: string
   commitCommitCommitter:
+    x-marshmallow-inline: commit
     type: object
     properties:
       date:
@@ -62,6 +67,7 @@ definitions:
       name:
         type: string
   commitCommitAuthor:
+    x-marshmallow-inline: commit
     type: object
     properties:
       date:
@@ -72,6 +78,7 @@ definitions:
       name:
         type: string
   commitAuthor:
+    x-marshmallow-inline: commit
     type: object
     properties:
       avatar_url:
diff --git a/swagger_marshmallow_codegen/tests/legacy_src/02empty.yaml b/swagger_marshmallow_codegen/tests/legacy_src/02empty.yaml
index ee7fb1f..5cb371e 100644
--- a/swagger_marshmallow_codegen/tests/legacy_src/02empty.yaml
+++ b/swagger_marshmallow_codegen/tests/legacy_src/02empty.yaml
@@ -5,9 +5,11 @@ definitions:
       empties:
         $ref: '#/definitions/wrapEmpties'
   wrapEmpties:
+    x-marshmallow-inline: wrap
     type: array
     items:
       $ref: '#/definitions/wrapEmptiesItem'
   wrapEmptiesItem:
+    x-marshmallow-inline: wrap
     type: object
     title: empty
diff --git a/swagger_marshmallow_codegen/tests/test_codegen.py b/swagger_marshmallow_codegen/tests/test_codegen.py
index 58de013..46c8f53 100644
--- a/swagger_marshmallow_codegen/tests/test_codegen.py
+++ b/swagger_marshmallow_codegen/tests/test_codegen.py
@@ -26,14 +26,14 @@ def test_v2(
     get_codegen().codegen(
         lifting_definition(d),
         {
-            "schema": True,
-            "input": True,
-            "output": True,
+            "emit_schema": True,
+            "emit_input": True,
+            "emit_output": True,
+            "header_comment": "",
             "emit_schema_even_primitive_type": True,
             "additional_properties_default": False,
         },
         ctx=ctx,
-        test=True,
     )
 
     expected = load_dstfile(dst_dir / dst_file, here=here).rstrip("\n")
@@ -59,14 +59,14 @@ def test_v3(
     get_codegen().codegen(
         lifting_definition(d),
         {
-            "schema": True,
-            "input": True,
-            "output": True,
+            "emit_schema": True,
+            "emit_input": True,
+            "emit_output": True,
+            "header_comment": "",
             "emit_schema_even_primitive_type": True,
             "additional_properties_default": False,
         },
         ctx=ctx,
-        test=True,
     )
 
     expected = load_dstfile(dst_dir / dst_file, here=here).rstrip("\n")
diff --git a/swagger_marshmallow_codegen/tests/test_codegen_legacy.py b/swagger_marshmallow_codegen/tests/test_codegen_legacy.py
index c7a87b2..3045da9 100644
--- a/swagger_marshmallow_codegen/tests/test_codegen_legacy.py
+++ b/swagger_marshmallow_codegen/tests/test_codegen_legacy.py
@@ -52,8 +52,16 @@ here = pathlib.Path(__file__).parent
         ("./legacy_src/00primitiveapi.yaml", "./legacy_dst/00primitiveapi.py", ""),
         # ("./legacy_src/00patternProperties.yaml", "./legacy_dst/00patternProperties.py"),  not supported yet
         # x-marshmallow-name
-        ("./legacy_src/00x_marshmallow_name.yaml", "./legacy_dst/00x_marshmallow_name.py", ""),
-        ("./legacy_src/00x_marshmallow_name.yaml", "./legacy_dst/00x_marshmallow_name.py", ""),
+        (
+            "./legacy_src/00x_marshmallow_name.yaml",
+            "./legacy_dst/00x_marshmallow_name.py",
+            "",
+        ),
+        (
+            "./legacy_src/00x_marshmallow_name.yaml",
+            "./legacy_dst/00x_marshmallow_name.py",
+            "",
+        ),
     ],
 )
 def test(
@@ -68,9 +76,9 @@ def test(
     get_codegen().codegen(
         lifting_definition(d),
         {
-            "schema": True,
-            "input": True,
-            "output": True,
+            "emit_schema": True,
+            "emit_input": True,
+            "emit_output": True,
             "header_comment": header_comment,
             "additional_properties_default": False,
         },
diff --git a/swagger_marshmallow_codegen/tests/test_lifting.py b/swagger_marshmallow_codegen/tests/test_lifting.py
index 3420174..cdff58d 100644
--- a/swagger_marshmallow_codegen/tests/test_lifting.py
+++ b/swagger_marshmallow_codegen/tests/test_lifting.py
@@ -1,37 +1,27 @@
-import unittest
-import os.path
-
-
-class FlattenTests(unittest.TestCase):
-    here = os.path.normpath(os.path.dirname(os.path.abspath(__file__)))
-
-    def _callFUT(self, d):
-        from swagger_marshmallow_codegen.lifting import lifting_definition
-
-        return lifting_definition(d)
-
-    def load_srcfile(self, src):
-        from swagger_marshmallow_codegen.loading import load
-
-        with open(os.path.join(self.here, src)) as rf:
-            return load(rf)
-
-    def load_dstfile(self, dst):
-        from swagger_marshmallow_codegen.loading import load
-
-        with open(os.path.join(self.here, dst)) as rf:
-            return load(rf)
-
-    def test_it(self):
-        candidates = [
-            ("./legacy_src/01commit.yaml", "./legacy_src/00commit.yaml"),
-            ("./legacy_src/01empty.yaml", "./legacy_src/02empty.yaml"),
-        ]
-        for src_file, dst_file in candidates:
-            with self.subTest(src_file=src_file, dst_file=dst_file):
-                noflatten = self.load_srcfile(src_file)
-                actual = self._callFUT(noflatten)
-                flatten = self.load_dstfile(dst_file)
-                from dictknife import deepequal
-
-                self.assertTrue(deepequal(flatten, actual))
+from __future__ import annotations
+import pathlib
+import json
+import pytest
+from .testing import load_srcfile
+
+here = pathlib.Path(__file__).parent
+
+
+@pytest.mark.parametrize(
+    "unflatten_file, flatten_file",
+    [
+        ("./legacy_src/01commit.yaml", "./legacy_src/00commit.yaml"),
+        ("./legacy_src/01empty.yaml", "./legacy_src/02empty.yaml"),
+    ],
+)
+def test(
+    unflatten_file, flatten_file,
+):
+    from swagger_marshmallow_codegen.lifting import lifting_definition as _callFUT
+
+    d = load_srcfile(unflatten_file, here=here)
+    got = _callFUT(d)
+    want = load_srcfile(flatten_file, here=here)
+    assert json.dumps(got, indent=2, sort_keys=True) == json.dumps(
+        want, indent=2, sort_keys=True
+    )
diff --git a/swagger_marshmallow_codegen/tests/testing.py b/swagger_marshmallow_codegen/tests/testing.py
index 8428d51..730f833 100644
--- a/swagger_marshmallow_codegen/tests/testing.py
+++ b/swagger_marshmallow_codegen/tests/testing.py
@@ -12,7 +12,9 @@ def get_codegen():
     return _TargetClass(resolver=resolver)
 
 
-def load_srcfile(src: pathlib.Path, *, here: pathlib.Path) -> t.Dict[str, t.Any]:
+def load_srcfile(
+    src: t.Union[str, pathlib.Path], *, here: pathlib.Path
+) -> t.Dict[str, t.Any]:
     from swagger_marshmallow_codegen.loading import load
 
     filepath = here / src
@@ -20,7 +22,7 @@ def load_srcfile(src: pathlib.Path, *, here: pathlib.Path) -> t.Dict[str, t.Any]
         return load(rf)
 
 
-def load_dstfile(dst: pathlib.Path, *, here: pathlib.Path) -> str:
+def load_dstfile(dst: t.Union[str, pathlib.Path], *, here: pathlib.Path) -> str:
     try:
         filepath = here / dst
         with filepath.open() as rf: