Repository: allmonday/pydantic-resolve (language: python)

Issue:
simple version of debug
- report time cost of each loader instance, and also the count of calls
- display the relationship of schemas
- list necessary loader params 
- a tool scan all dataloaders and analysis their configs to build the ERD map
[doc] udpate readme



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 3db5047..f616977 100644
--- a/README.md
+++ b/README.md
@@ -3,13 +3,11 @@
 ![Python Versions](https://img.shields.io/pypi/pyversions/pydantic-resolve)
 [![CI](https://github.com/allmonday/pydantic_resolve/actions/workflows/ci.yml/badge.svg)](https://github.com/allmonday/pydantic_resolve/actions/workflows/ci.yml)
 
-Pydantic-resolve is a schema based solution for data management.
+Pydantic-resolve is a schema based solution for data composition, it can provide you with 3 ~ 5 times the increase in development efficiency and reduce the amount of code by more than 50%.
 
-1. manage the deep data inside it's schema, instead of visiting if outside by manual traversal.
-2. pydantic-resolve runs a Level Order Traversal (BFS) inside and execute `resolve` and `post` during this process.
-3. describe the relationship between data in a way close to ERD (entity relationship diagram)
-
-![](./doc/imgs/concept.png)
+1. It manages the deep data inside each schema, instead of visiting from outside by manual traversal.
+2. It runs a Level Order Traversal (BFS) inside and execute `resolve` and `post` during this process.
+3. It describes the relationship between data in a form close to ERD (entity relationship diagram)
 
 ## Install
 
@@ -96,7 +94,9 @@ output
 }
 ```
 
-## Composing a subset of ERD
+## Composing a subset from ERD definitions
+
+![](./doc/imgs/concept.png)
 
 define elements of ERD, schema (entity), dataloader (relationship).
 
@@ -166,7 +166,7 @@ async def main():
 asyncio.run(main())
 ```
 
-output 
+output
 
 ```json
 [
@@ -211,8 +211,6 @@ output
 ]
 ```
 
-
-
 ## Documents
 
 - **Quick start**: https://allmonday.github.io/pydantic-resolve/about/
diff --git a/docs/about.en.md b/docs/v1/about.en.md
similarity index 100%
rename from docs/about.en.md
rename to docs/v1/about.en.md
diff --git a/docs/about.zh.md b/docs/v1/about.zh.md
similarity index 100%
rename from docs/about.zh.md
rename to docs/v1/about.zh.md
diff --git a/docs/changelog-v2.en.md b/docs/v1/changelog-v2.en.md
similarity index 100%
rename from docs/changelog-v2.en.md
rename to docs/v1/changelog-v2.en.md
diff --git a/docs/changelog-v2.zh.md b/docs/v1/changelog-v2.zh.md
similarity index 100%
rename from docs/changelog-v2.zh.md
rename to docs/v1/changelog-v2.zh.md
diff --git a/docs/changelog.en.md b/docs/v1/changelog.en.md
similarity index 100%
rename from docs/changelog.en.md
rename to docs/v1/changelog.en.md
diff --git a/docs/changelog.zh.md b/docs/v1/changelog.zh.md
similarity index 100%
rename from docs/changelog.zh.md
rename to docs/v1/changelog.zh.md
diff --git a/docs/dataloader.en.md b/docs/v1/dataloader.en.md
similarity index 100%
rename from docs/dataloader.en.md
rename to docs/v1/dataloader.en.md
diff --git a/docs/dataloader.zh.md b/docs/v1/dataloader.zh.md
similarity index 100%
rename from docs/dataloader.zh.md
rename to docs/v1/dataloader.zh.md
diff --git a/docs/index.en.md b/docs/v1/index.en.md
similarity index 100%
rename from docs/index.en.md
rename to docs/v1/index.en.md
diff --git a/docs/index.zh.md b/docs/v1/index.zh.md
similarity index 100%
rename from docs/index.zh.md
rename to docs/v1/index.zh.md
diff --git a/docs/inherit.en.md b/docs/v1/inherit.en.md
similarity index 100%
rename from docs/inherit.en.md
rename to docs/v1/inherit.en.md
diff --git a/docs/inherit.zh.md b/docs/v1/inherit.zh.md
similarity index 100%
rename from docs/inherit.zh.md
rename to docs/v1/inherit.zh.md
diff --git a/docs/philosophy.en.md b/docs/v1/philosophy.en.md
similarity index 100%
rename from docs/philosophy.en.md
rename to docs/v1/philosophy.en.md
diff --git a/docs/philosophy.zh.md b/docs/v1/philosophy.zh.md
similarity index 100%
rename from docs/philosophy.zh.md
rename to docs/v1/philosophy.zh.md
diff --git a/docs/reference_api.en.md b/docs/v1/reference_api.en.md
similarity index 100%
rename from docs/reference_api.en.md
rename to docs/v1/reference_api.en.md
diff --git a/docs/reference_api.zh.md b/docs/v1/reference_api.zh.md
similarity index 100%
rename from docs/reference_api.zh.md
rename to docs/v1/reference_api.zh.md
diff --git a/docs/tree.en.md b/docs/v1/tree.en.md
similarity index 100%
rename from docs/tree.en.md
rename to docs/v1/tree.en.md
diff --git a/docs/v2/api.md b/docs/v2/api.md
new file mode 100644
index 0000000..89b9c6f
--- /dev/null
+++ b/docs/v2/api.md
@@ -0,0 +1,349 @@
+# API
+
+## Methods
+
+### resolve
+
+The `resolve_field` method can be async, and the Resolver will recursively resolve all `resolve_field` methods in the child nodes to obtain data.
+
+Usable parameters:
+
+- context
+- ancestor_context
+- parent
+- dataloaders (supports multiple)
+
+```python
+class Blog(BaseModel):
+    id: int
+
+    comments: list[str] = []
+    def resolve_comments(self):
+        return ['comment-1', 'comment-2']
+
+    tags: list[str] = []
+    async def resolve_tags(self):
+        await asyncio.sleep(1)
+        return ['tag-1', 'tag-2']
+```
+
+### post
+
+The `post_field` method can be sync or async, and it is triggered after the data of the descendant nodes is processed, used for subsequent processing of the obtained data.
+
+Usable parameters:
+
+- context
+- ancestor_context
+- parent
+- collectors (supports multiple)
+
+```python
+class Blog(BaseModel):
+    id: int
+
+    comments: list[str] = []
+    def resolve_comments(self):
+        return ['comment-1', 'comment-2']
+
+    def post_comments(self):
+        return self.comments[-1:] # keep the last one
+```
+
+### post_default_handler
+
+`post_default_handler` is a special post method that is executed after all post methods are executed. It is used for handling some finishing work.
+
+Note that this method does not have automatic assignment logic, you need to manually specify it.
+
+Usable parameters:
+
+- context
+- ancestor_context
+- parent
+- collectors (supports multiple)
+
+```python
+class Blog(BaseModel):
+    id: int
+
+    length: int
+
+    def post_default_handler(self):
+        self.length = 100
+```
+
+## Resolver
+
+The entry point for pydantic-resolve execution
+
+```python
+
+class Resolver:
+    def __init__(
+            self,
+            loader_params: Optional[Dict[Any, Dict[str, Any]]] = None,
+            global_loader_param: Optional[Dict[str, Any]] = None,
+            loader_instances: Optional[Dict[Any, Any]] = None,
+            context: Optional[Dict[str, Any]] = None):
+```
+
+- `loader_params`: Used to provide parameters for DataLoader
+- `copy_dataloader_kls`: Used to batch set DataLoader parameters
+- `loader_instances`: Can pass in DataLoader instances (pre-filled data)
+- `context`: Provides global parameters
+
+## Method Parameter Description
+
+### context
+
+`context` is a global context, set in the Resolver method, and can be accessed by all methods.
+
+```python hl_lines="5 9"
+class Blog(BaseModel):
+    id: int
+
+    comments: list[str] = []
+    def resolve_comments(self, context):
+        prefix = context['prefix']
+        return [f'{prefix}-{c}' for c in ['comment-1', 'comment-2']]
+
+    def post_comments(self, context):
+        limit = context['limit']
+        return self.comments[-limit:]  # get last [limit] comments
+
+blog = Blog(id=1)
+blog = await Resolver(context={'prefix': 'my', 'limit': 1}).resolve(blog)
+```
+
+### ancestor_context
+
+In some scenarios, we may need to obtain data from the ancestor nodes of a certain node, which can be achieved through `ancestor_context`.
+
+First, you need to add the `__pydantic_resolve_expose__` parameter in the ancestor node to configure the field names and aliases to be provided (in case of name conflicts in the hierarchy).
+
+Then you can read it through `ancestor_context`.
+
+```python hl_lines="2 18"
+class Blog(BaseModel):
+    __pydantic_resolve_expose__ = {'title': 'blog_title' }
+    id: int
+    title: str
+
+    comments: list[Comment] = []
+    def resolve_comments(self, loader=LoaderDepend(blog_to_comments_loader)):
+        return loader.load(self.id)
+
+    comment_count: int = 0
+    def post_comment_count(self):
+        return len(self.comments)
+
+class Comment(BaseModel):
+    id: int
+    content: str
+    def post_content(self, ancestor_context):
+        blog_title = ancestor_context['blog_title']
+        return f'[{blog_title}] - {self.content}'
+```
+
+### parent
+
+You can get your direct parent node, which is particularly useful in tree structures.
+
+```python hl_lines="6-8"
+class Tree(BaseModel):
+    name: str
+    children: List[Tree] = []
+
+    path: str = ''
+    def resolve_path(self, parent):
+        if parent is not None:
+            return f'{parent.path}/{self.name}'
+        return self.name
+
+data = dict(name="a", children=[
+    dict(name="b", children=[
+        dict(name="c")
+    ]),
+    dict(name="d", children=[
+        dict(name="c")
+    ])
+])
+data = await Resolver().resolve(Tree(**data))
+```
+
+### collector
+
+`collector` can be used to obtain data from descendant nodes across generations, and it needs to be used in conjunction with `Collector` and the `__pydantic_resolve_collect__` parameter.
+
+Define `__pydantic_resolve_collect__` in the descendant nodes to specify the field information/collector name to be provided.
+
+`collector` allows developers to flexibly adjust the data structure without having to loop through the descendant nodes.
+
+For example, we can collect the comment information of each blog in the top-level schema.
+
+```python hl_lines="13 18"
+form pydantic_resolve import Collector
+
+class MyBlogSite(BaseModel):
+    blogs: list[Blog] = []
+    async def resolve_blogs(self):
+        return await get_blogs()
+
+    comment_count: int = 0
+    def post_comment_count(self):
+        return sum([b.comment_count for b in self.blogs])
+
+    all_comments: list[Comment] = []
+    def post_all_comments(self, collector=Collector(alias='blog_comments', flat=True)):
+        return collector.values()
+
+class Blog(BaseModel):
+    __pydantic_resolve_expose__ = {'title': 'blog_title' }
+    __pydantic_resolve_collect__ = {'comments': 'blog_comments' }
+    id: int
+    title: str
+
+    comments: list[Comment] = []
+    def resolve_comments(self, loader=LoaderDepend(blog_to_comments_loader)):
+        return loader.load(self.id)
+
+    comment_count: int = 0
+    def post_comment_count(self):
+        return len(self.comments)
+
+class Comment(BaseModel):
+    id: int
+    content: str
+    def post_content(self, ancestor_context):
+        blog_title = ancestor_context['blog_title']
+        return f'[{blog_title}] - {self.content}'
+```
+
+1. `collector` supports creating multiple instances
+2. `Collector` will use an array to accumulate data by default, `flat=True` will use `extend` to merge data internally
+3. You can inherit `ICollector` to create custom collectors
+
+```python
+from pydantic_resolve import ICollector
+
+class CounterCollector(ICollector):
+    def __init__(self, alias):
+        self.alias = alias
+        self.counter = 0
+
+    def add(self, val):
+        self.counter = self.counter + len(val)
+
+    def values(self):
+        return self.counter
+```
+
+### dataloader
+
+`DataLoader` can merge multiple concurrent asynchronous queries into one.
+
+In pydantic-resolve, you need to use `LoaderDepend` to manage `DataLoader`.
+
+A method can declare multiple `DataLoader`s.
+
+```python
+from pydantic_resolve import LoaderDepend
+
+class Blog(BaseModel):
+    id: int
+    title: str
+
+    comments: list[Comment] = []
+    async def resolve_comments(self,
+                         loader1=LoaderDepend(blog_to_comments_loader),
+                         loader2=LoaderDepend(blog_to_comments_loader2)):
+        v1 = await loader1.load(self.id)  # list
+        v2 = await loader2.load(self.id)  # list
+        return v1 + v2
+```
+
+If the `DataLoader` has class variables defined, you can provide parameters in the `Resolver` method.
+
+```python hl_lines="2 7"
+class LoaderA(DataLoader):
+    power: int
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power for k in keys ]
+
+data = await Resolver(loader_filters={LoaderA:{'power': 2}}).resolve(data)
+```
+
+If multiple `DataLoader`s of the same type use the same parameters, you can use `global_loader_param` to simplify the parameter settings.
+
+Use with caution, as parameter maintenance may become unclear.
+
+## Auxiliary Methods
+
+### build_list, build_object
+
+Used in `DataLoader` to aggregate the obtained data according to the keys.
+
+`build_list` returns an array of objects, `build_object` returns an object.
+
+Signature: `build_list(data, keys, lambda d: d.key)`
+
+### model_config
+
+Using `exclude` can remove fields when converting pydantic to the target data, but doing so alone will still keep the `name` field in the definition when generating `openapi.json` in FastAPI. Adding the `model_config()` decorator can remove `name`.
+
+Signature: `model_config(default_required=True)`
+
+```python
+@model_config()
+class Data(BaseModel):
+    name: str = Field('', exclude=True)
+```
+
+### ensure_subset
+
+Signature: `ensure_subset(base_kls)`
+
+If only a subset of fields is needed, but you want to strictly ensure the subset, you can use `ensure_subset` to check.
+
+If the Base changes and the field does not exist, an `AttributeError` will be thrown.
+
+```python
+class Base(BaseModel):
+    a: str
+    b: int
+
+@ensure_subset(Base)
+class ChildA(BaseModel):
+    a: str
+```
+
+### mapper
+
+Provides a data conversion decorator
+
+```python
+class Data(BaseModel):
+    id: int
+
+    items: List[Item] = []
+
+    @mapper(lambda x: do_some_conversion(x))
+    def resolve_items(self, loader=LoaderDepend(ItemLoader)):
+        return loader.load(self.id)
+```
+
+### copy_dataloader_kls
+
+Copies a `DataLoader` to handle cases where different parameters need to be passed to `DataLoader`.
+
+```python
+NewLoader = copy_dataloader_kls('NewLoader', OriginLoader)
+```
+
+## Exceptions
+
+- `ResolverTargetAttrNotFound`: Target field not found
+- `LoaderFieldNotProvidedError`: Required parameters for Loader not provided in Resolve
+- `GlobalLoaderFieldOverlappedError`: Duplicate parameters in `global_loader_params` and `loader_params`
+- `MissingCollector`: Target collector not found, not defined in ancestor node methods
diff --git a/docs/v2/api.zh.md b/docs/v2/api.zh.md
new file mode 100644
index 0000000..fb98b32
--- /dev/null
+++ b/docs/v2/api.zh.md
@@ -0,0 +1,349 @@
+# API
+
+## 方法
+
+### resolve
+
+resolve_field 方法可以是 async 的， Resolver 会递归的解析子节点中的所有 resolve_field 方法来获取数据
+
+可以使用的参数
+
+- context
+- ancestor_context
+- parent
+- dataloaders（支持多个）
+
+```python
+class Blog(BaseModel):
+    id: int
+
+    comments: list[str] = []
+    def resolve_comments(self):
+        return ['comment-1', 'comment-2']
+
+    tags: list[str] = []
+    async def resolve_tags(self):
+        await asyncio.sleep(1)
+        return ['tag-1', 'tag-2']
+```
+
+### post
+
+post_field 方法可以为 sync 或者 async, 在子孙节点的数据处理完毕之后触发，用来对获取到的数据做后续处理。
+
+可以使用的参数:
+
+- context
+- ancestor_context
+- parent
+- collectors (支持多个)
+
+```python
+class Blog(BaseModel):
+    id: int
+
+    comments: list[str] = []
+    def resolve_comments(self):
+        return ['comment-1', 'comment-2']
+
+    def post_comments(self):
+        return self.comments[-1:] # keep the last one
+```
+
+### post_default_handler
+
+`post_default_handler` 是一个特殊的 post 方法， 他会在所有 post 方法执行完毕之后执行。 适用于处理一些收尾工作。
+
+注意该方法没有自动赋值的逻辑， 需要自己手动指定。
+
+可以使用的参数:
+
+- context
+- ancestor_context
+- parent
+- collectors (支持多个)
+
+```python
+class Blog(BaseModel):
+    id: int
+
+    length: int
+
+    def post_default_handler(self):
+        self.length = 100
+```
+
+## Resolver
+
+pydantic-resolve 的执行入口
+
+```python
+
+class Resolver:
+    def __init__(
+            self,
+            loader_params: Optional[Dict[Any, Dict[str, Any]]] = None,
+            global_loader_param: Optional[Dict[str, Any]] = None,
+            loader_instances: Optional[Dict[Any, Any]] = None,
+            context: Optional[Dict[str, Any]] = None):
+```
+
+- `loader_params` 用来提供 DataLoader 的参数
+- `copy_dataloader_kls` 用来批量设置 DataLoader 参数
+- `loader_instances` 可以传入 DataLoader 实例 （提前写入数据）
+- `context`: 提供全局参数
+
+## 方法参数说明
+
+### context
+
+context 是一个全局上下文， 在 Resolver 方法中设置， 可以被所有方法获取到。
+
+```python hl_lines="5 9"
+class Blog(BaseModel):
+    id: int
+
+    comments: list[str] = []
+    def resolve_comments(self, context):
+        prefix = context['prefix']
+        return [f'{prefix}-{c}' for c in ['comment-1', 'comment-2']]
+
+    def post_comments(self, context):
+        limit = context['limit']
+        return self.comments[-limit:]  # get last [limit] comments
+
+blog = Blog(id=1)
+blog = await Resolver(context={'prefix': 'my', 'limit': 1}).resolve(blog)
+```
+
+### ancestor_context
+
+在一些场景中， 我们可能需要获取某个节点的祖先节点中的数据， 就可以通过 ancestor_context 来实现
+
+首先你需要在祖先节点中添加 `__pydantic_resolve_expose__` 参数来配置要提供的字段名称和别名（层叠中发生重名）
+
+然后就能通过 ancestor_context 来读取到了。
+
+```python hl_lines="2 18"
+class Blog(BaseModel):
+    __pydantic_resolve_expose__ = {'title': 'blog_title' }
+    id: int
+    title: str
+
+    comments: list[Comment] = []
+    def resolve_comments(self, loader=LoaderDepend(blog_to_comments_loader)):
+        return loader.load(self.id)
+
+    comment_count: int = 0
+    def post_comment_count(self):
+        return len(self.comments)
+
+class Comment(BaseModel):
+    id: int
+    content: str
+    def post_content(self, ancestor_context):
+        blog_title = ancestor_context['blog_title']
+        return f'[{blog_title}] - {self.content}'
+```
+
+### parent
+
+可以获得自己的直接父节点， 在 tree 结构中特别有用。
+
+```python hl_lines="6-8"
+class Tree(BaseModel):
+    name: str
+    children: List[Tree] = []
+
+    path: str = ''
+    def resolve_path(self, parent):
+        if parent is not None:
+            return f'{parent.path}/{self.name}'
+        return self.name
+
+data = dict(name="a", children=[
+    dict(name="b", children=[
+        dict(name="c")
+    ]),
+    dict(name="d", children=[
+        dict(name="c")
+    ])
+])
+data = await Resolver().resolve(Tree(**data))
+```
+
+### collector
+
+collector 可以用来跨代获取子孙节点的数据， 需要配合 `Collector` 和 `__pydantic_resolve_collect__` 参数使用
+
+在子孙节点中定义 `__pydantic_resolve_collect__` 来指定需要提供的字段信息/收集者名字。
+
+collector 可以让开发者灵活地调整数据结构，不需要去循环地展开子孙节点。
+
+比如， 我们可以在顶层 schema 中收集每个 blog 的 comment 信息。
+
+```python hl_lines="13 18"
+form pydantic_resolve import Collector
+
+class MyBlogSite(BaseModel):
+    blogs: list[Blog] = []
+    async def resolve_blogs(self):
+        return await get_blogs()
+
+    comment_count: int = 0
+    def post_comment_count(self):
+        return sum([b.comment_count for b in self.blogs])
+
+    all_comments: list[Comment] = []
+    def post_all_comments(self, collector=Collector(alias='blog_comments', flat=True)):
+        return collector.values()
+
+class Blog(BaseModel):
+    __pydantic_resolve_expose__ = {'title': 'blog_title' }
+    __pydantic_resolve_collect__ = {'comments': 'blog_comments' }
+    id: int
+    title: str
+
+    comments: list[Comment] = []
+    def resolve_comments(self, loader=LoaderDepend(blog_to_comments_loader)):
+        return loader.load(self.id)
+
+    comment_count: int = 0
+    def post_comment_count(self):
+        return len(self.comments)
+
+class Comment(BaseModel):
+    id: int
+    content: str
+    def post_content(self, ancestor_context):
+        blog_title = ancestor_context['blog_title']
+        return f'[{blog_title}] - {self.content}'
+```
+
+1. collector 支持创建多个
+2. `Collector` 默认会使用数组来叠加数据， flat=True 会在内部使用 extend 合并数据
+3. 可以继承 `ICollector` 来创建自定义的收集器
+
+```python
+from pydantic_resolve import ICollector
+
+class CounterCollector(ICollector):
+    def __init__(self, alias):
+        self.alias = alias
+        self.counter = 0
+
+    def add(self, val):
+        self.counter = self.counter + len(val)
+
+    def values(self):
+        return self.counter
+```
+
+### dataloader
+
+DataLoader 可以将并发的多个异步查询合并为一个。
+
+在 pydantic-resolve 中需要使用 LoaderDepend 来管理 DataLoader。
+
+支持一个方法中申明多个 DataLoader。
+
+```python
+from pydantic_resolve import LoaderDepend
+
+class Blog(BaseModel):
+    id: int
+    title: str
+
+    comments: list[Comment] = []
+    async def resolve_comments(self,
+                         loader1=LoaderDepend(blog_to_comments_loader),
+                         loader2=LoaderDepend(blog_to_comments_loader2)):
+        v1 = await loader1.load(self.id)  # list
+        v2 = await loader2.load(self.id)  # list
+        return v1 + v2
+```
+
+如果 DataLoader 中有定义类变量， 可以在 Resolver 方法中提供参数。
+
+```python hl_lines="2 7"
+class LoaderA(DataLoader):
+    power: int
+    async def batch_load_fn(self, keys: List[int]):
+        return [ k** self.power for k in keys ]
+
+data = await Resolver(loader_filters={LoaderA:{'power': 2}}).resolve(data)
+```
+
+如果多个相同类型的 DataLoader 使用了相同的参数， 可以使用 `global_loader_param` 来简化设置参数。
+
+请慎用， 参数维护可能会不清晰。
+
+## 辅助方法
+
+### build_list, build_object
+
+在 DataLoader 中用来将获取到的数据根据 keys 做聚合。
+
+build_list 返回对象数组， build_object 返回对象。
+
+签名 `build_list(data, keys, lambda d: d.key)`
+
+### model_config
+
+使用 exclude 可以在 pydantic 转换成目标数据时移除字段， 但是光这么做在 FastAPI 中生成 openapi.json 时 `name` 字段依然会存在于定义中， 添加 model_config() 装饰器可以移除 `name`。
+
+签名 `model_config(default_required=True)`
+
+```python
+@model_config()
+class Data(BaseModel):
+    name: str = Field('', exclude=True)
+```
+
+### ensure_subset
+
+签名 `ensure_subset(base_kls)`
+
+如果只需要一部分字段， 但有希望严格确保子集， 可以使用 `ensure_subset` 来检查。
+
+如果 Base 做了变化导致字段不存在， 会抛出 AttributeError
+
+```python
+class Base(BaseModel):
+    a: str
+    b: int
+
+@ensure_subset(Base)
+class ChildA(BaseModel):
+    a: str
+```
+
+### mapper
+
+提供数据转换装饰器
+
+```python
+class Data(BaseModel):
+    id: int
+
+    items: List[Item] = []
+
+    @mapper(lambda x: do_some_conversion(x))
+    def resolve_items(self, loader=LoaderDepend(ItemLoader)):
+        return loader.load(self.id)
+```
+
+### copy_dataloader_kls
+
+拷贝一份 DataLoader， 用来处理带参数的 DataLoader 需要传递不同参数的情况。
+
+```python
+NewLoader = copy_dataloader_kls('NewLoader', OriginLoader)
+```
+
+## 异常
+
+- `ResolverTargetAttrNotFound`: 目标 field 不存在
+- `LoaderFieldNotProvidedError`: Resolve 中没有提供 Loader 所需的参数
+- `GlobalLoaderFieldOverlappedError`: `global_loader_params` 和 `loader_params` 参数出现重复
+- `MissingCollector`: 找不到目标 collector, 祖先节点方法中未定义
diff --git a/docs/v2/changelog.md b/docs/v2/changelog.md
new file mode 100644
index 0000000..dc19cde
--- /dev/null
+++ b/docs/v2/changelog.md
@@ -0,0 +1,409 @@
+# Changelog
+
+## v1.11.1 (2024.11.21)
+
+- refactor fold structure
+- new documentation
+
+## v1.11.0 (2024.10.24)
+
+- support pydantic v2
+- remove hidden_fields in model_config
+
+## v1.10.8 (2024.8.27)
+
+- collector suppor tuple for value
+
+```python
+class Item(BaseModel):
+    __pydantic_resolve_collect__ = {('field_a', 'field_b'): ('collector_a', 'collector_b')}
+```
+
+this means `(Item.field_a, Item.field_b)` will be sent to both collector_a and collector_b
+
+## v1.10.7 (2024.8.16)
+
+- collector support tuple
+
+```python
+class Item(BaseModel):
+    __pydantic_resolve_collect__ = {('field_a', 'field_b'): 'collector_name'}
+```
+
+## v1.10.6 (2024.8.12)
+
+- collector now can collect from multiple sources
+
+## v1.10.5 (2024.5.20)
+
+- post method can be async function
+
+## v1.10.4 (2024.4.11)
+
+- new feature: reading parent node in resolve, post and post_default_handler
+
+## v1.10.3 (2024.4.8)
+
+- bugfix, handle TypeError from issubclass, https://github.com/allmonday/pydantic2-resolve/issues/7
+
+## v1.10.2 (2024.3.21)
+
+- internal optimization:
+  - no more inspect.signature in runtime
+
+## v1.10.1 (2024.3.17)
+
+- fix collect intermediate level error
+- internal refactor
+  - better metadata lookup
+
+## v1.10.0 (2024.3.16) yanked
+
+- add new feature: `collector`
+- internal refactor
+  - optimize scan process.
+  - prepare for debug feature.
+  - create dataloader instance before resolving
+
+## v1.9.3 (2024.03.12)
+
+- rename loader_filter -> loader_params
+- rename global_loader_filter -> global_loader_param
+
+using old params will have warning messages.
+
+## v1.9.2 (2024.02.03)
+
+- bugfix for model_config
+  - setting `__exclude_fields__` should not put in schema_extra.
+
+## v1.9.1 (2024.02.02)
+
+- add new decorator `model_config`:
+  - control hidden fields (for schema() and dict())
+  - built-in required in schema
+
+## v1.9.0 (2024.01.22)
+
+- add `global_loader_filter` for convinence. (thanks Dambre)
+
+## v1.8.2 (2023.12.20)
+
+- fix corner case of empty list input. `tests/core/test_input.py`
+
+## v1.8.1 (2023.12.16)
+
+- fix scan exceptions caused from pydantic `.type_` value of `List[Optional[T]]`
+
+## v1.8.0 (2023.12.15)
+
+- internal refactor: performance improvement
+- remove Resolver.annotation_class, it will be processed automatically
+- new helper function `copy_dataloader_kls` to generate a copy of DataLoader
+- `pydantic_resolve.utils` provide several internal `generate_loader` functions.
+
+## v1.7.2 (2023.11.16)
+
+- fix overwriting BaseModel.Config.schema_extra in `@input`
+
+## v1.7.1 (2023.10.19)
+
+- fix raising error when `__pydantic_resolve_exposed__` field value is None
+
+```python
+class Bar(BaseModel):
+    __pydantic_resolve_expose__ = {'num': 'bar_num'}  # expose {'bar_num': val }
+
+    num: Optional[int]  # may be None
+```
+
+## v1.7.0 (2023.9.2)
+
+- add a port in ancestor class to expose value of specific field to its descendants.
+- resolve_field and post_field of descendant class can read those from `ancestor_context`
+- DO NOT EXPOSE resolver_fields (eg Bar.kars, Kar.desc), it is empty by default.
+
+```python
+class Kar(BaseModel):
+    name: str
+
+    desc: str = ''
+    def resolve_desc(self, ancestor_context):
+        return f"{self.name} - {ancestor_context['bar_num']}"  # read ancestor value from 'ancestor_context'
+
+
+class Bar(BaseModel):
+    __pydantic_resolve_expose__ = {'num': 'bar_num'}  # expose {'bar_num': val }
+
+    num: int
+
+    kars: List[Kar] = []
+    def resolve_kars(self):
+        return [{'name': n} for n in ['a', 'b', 'c']]
+
+
+class Foo(BaseModel):
+    nums:List[int]
+    bars: List[Bar] = []
+
+    def resolve_bars(self):
+        return [{'num': n} for n in self.nums]
+
+```
+
+## v1.6.5 (2023.8.12)
+
+- `post_field` method and `post_default_handler` now can read context (setting in Resolver)
+
+## v1.6.4 (2023.8.7)
+
+- some inside refactor
+
+## v1.6.3 (2023.7.20)
+
+- fix dataclass exception under lazy annotation. refer to `tests/resolver/test_28_parse_to_obj_for_dataclass_with_annotation.py`
+- provide extra tip on auto map fails.
+
+> recursion dataclass type is still not supported, refer to `test/resolver/test_26_tree.py`, L81
+
+## v1.6.2 (2023.7.12)
+
+- fix `output` minor bug
+
+## v1.6.1 (2023.7.11)
+
+- fix `output` decorator, it will modify schema_json's `required` field and fill all fields
+
+## v1.6.0 (2023.7.11)
+
+- add `Resolver.context` param
+- remove `core.resolve`
+- change `post_method`, return value will be assined to target field.
+- add `output` decorator
+- add `post_default_handler` for spacial use.
+
+## v1.5.2 (2023.7.10)
+
+- fix pydantic annotation related minor issue
+
+## v1.5.1 (2023.7.9)
+
+- fix the order of auto map, which will break the resolve chain
+
+## v1.5.0 (2023.7.9) yanked
+
+- new feature. the return value from resolve_method will be automatically converted to the type of target field, which means mapper with params of type is not required any more.
+- if you `fromt __future__ import annotations` at top, make sure you define schemas in global scopes.
+
+```python
+    class Book(BaseModel):
+        name: str
+
+    async def batch_load_fn(keys):
+        print('oader')
+        books = [[bb for bb in BOOKS.get(k, [])] for k in keys]
+        return books
+
+    class Student(BaseModel):
+        id: int
+        name: str
+
+        books: List[Book] = []
+        def resolve_books(self, loader=LoaderDepend(batch_load_fn)):
+            return loader.load(self.id)  # auto convert dict into Book type
+
+    class ClassRoom(BaseModel):
+        students: List[Student]
+
+
+    students = [Student(id=1, name="jack"), Student(id=2, name="mike"), Student(id=3, name="wiki")]
+    classroom = ClassRoom(students=students)
+    classroom = await Resolver().resolve(classroom)
+    assert isinstance(classroom.students[0].books[0], Book)
+```
+
+- recommend: using `mapper` with lambda only
+
+## v1.4.1 (2023.7.6)
+
+- minor optimization, iteration of object attributes.
+
+## v1.4.0 (2023.7.6)
+
+- support resolve through objects which intermediate items has not `resolve_` methods.
+
+more detail: `tests/resolver/test_21_not_stop_by_idle_level.py`
+
+```python
+class C(BaseModel):
+    name: str = ''
+
+class B(BaseModel):
+    name: str
+    c: Optional[C] = None
+    async def resolve_c(self) -> Optional[C]:
+        await asyncio.sleep(1)
+        return C(name='hello world')
+
+class A(BaseModel):
+    b: B
+
+class Z(BaseModel):
+    a: A
+    resolve_age: int
+
+@pytest.mark.asyncio
+async def test_resolve_object():
+    s = Z(a=A(b=B(name="kikodo")), resolve_age=21)  # resolve starts from B
+
+    result = await Resolver().resolve(s)
+```
+
+## v1.3.2 (2023.7.4)
+
+- add subset check decorator `ensure_subset`.
+
+```python
+    class Base(BaseModel):
+        a: str
+        b: int
+
+    @util.ensure_subset(Base)
+    class ChildA(BaseModel):
+        a: str
+
+    @util.ensure_subset(Base)
+    class ChildB(BaseModel):
+        a: str
+        c: int = 0
+        def resolve_c(self):
+            return 21
+
+    @util.ensure_subset(Base)
+    class ChildB(BaseModel):
+        a: int  # raise attribute error
+```
+
+## v1.3.1 (2023.7.3)
+
+- support `auto-mapping` from pydantic to pydantic and fix some testcases.
+
+more detail: `tests/resolver/test_16_mapper.py`
+
+- test_mapper_6
+- test_mapper_7
+
+## v1.3.0 (2023.6.27)
+
+- add `Resolver.loader_instances` param, user can create loader before Resolver and this loader will be used inside. for example: you can prime value and to avoid extra query.
+
+```python
+loader = FriendLoader()
+loader.prime('tangkikodo', ['tom', 'jerry'])
+loader.prime('john', ['mike', 'wallace'])
+result = await Resolver(loader_instances={FriendLoader: loader}).resolve(root)
+# batch_load_fn will not run.
+```
+
+more detail: `tests/resolver/test_20_loader_instance.py`
+
+## v1.2.2 (2023.6.21)
+
+- minor adjustment, `build_list` and `build_object` will return iterator instead of list.
+
+## v1.2.1 (2023.6.19)
+
+- fix, modify `post_fieldname` execution position, reduce the duplication.
+
+## v1.2.0 (2023.6.18)
+
+- add `post_fieldname` method, it will be called after the object is fully resolve as a hook, developer can run some aggregation computation. `tests/resolver/test_18_post_methods.py`
+
+```python
+class Friend(BaseModel):
+    name: str
+
+    cash: Optional[Cash] = None
+    @mapper(Cash)  # auto mapping
+    def resolve_cash(self, contact_loader=LoaderDepend(cash_batch_load_fn)):
+        return contact_loader.load(self.name)
+
+    has_cash: bool = False
+    def post_has_cash(self):    # <----------------------
+        self.has_cash = self.cash is not None
+
+class User(BaseModel):
+    name: str
+    age: int
+
+    friends: List[Friend] = []
+    @mapper(lambda names: [Friend(name=name) for name in names])
+    def resolve_friends(self, friend_loader=LoaderDepend(friends_batch_load_fn)):
+        return friend_loader.load(self.name)
+
+    has_cash: bool = False
+    def post_has_cash(self):    # <----------------------
+        self.has_cash = any([f.has_cash for f in self.friends])
+```
+
+## v1.1.1 (2023.6.17)
+
+- extend @mapper decorator with target class option, this will call auto_mapping function inside.
+
+```python
+comments: List[CommentSchema]  = []
+@mapper(CommentSchema)
+def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+    return loader.load(self.id)
+```
+
+## v1.1.0 (2023.6.16)
+
+- add @mapper decorator, to enable custom data transform
+
+```python
+comments: List[CommentSchema]  = []
+@mapper(lambda items: [CommentSchema.from_orm(item) for item in items])
+def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+    return loader.load(self.id)
+```
+
+## v1.0.0 (2023.6.11)
+
+- support `batch_load_fn` as params for `LoaderDepend`
+- add test `tests/resolver/test_15_support_batch_load_fn.py`
+- `build_object`, `build_list` can be imported from `pydantic_resolve`
+
+## v0.5.1 (2023.6.11)
+
+- add helper utils for Dataloader.batch_load_fn `built_list` and `build_object`, see `examples/fastapi_demo/loader.py`
+- FIX: fix potential error caused by same loader name from different module. see `tests/resolver/test_14_check_loader_name.py`
+
+## v0.5.0 (2023.6.1)
+
+- `Resolver.ensure_type`: True will ensure `resolve_*` methods have return annotation.
+- add FastAPI integrated example `examples/fastapi_demo`
+
+## v0.4.0 (2023.4.6)
+
+- add new install option `pip install "pydantic-resolve[dataloader]" to include `aiodataloader` by default
+- add new `doc/loader-cn.md`, `doc/loader-en.md` to explain the convinence of using `LoaderDepen`
+- add new params in Resolver: `loader_filters` to support global filter setting for inside loaders.
+- add `examples/6_sqlalchemy_loaderdepend_global_filter.md` for global filter
+
+## v0.3.2 (2023.4.5)
+
+- refact tests, group by `core` and `resolver`
+- replace `unittest` with `pytest.mark.asyncio`
+- little `readme.md` change, new top sample code
+
+## v0.3.1 (2023.4.3)
+
+- change code examples in readme.md
+- add unittest for pydantic validationError
+- code refactor (rename)
+
+## v0.3.0
+
+- add `DataloaderDependCantBeResolved`, it will raise if use `resolve` to handle schema with `Dataloader`
+- add `chinese.md` and `english.md` in `doc` folder
diff --git a/docs/v2/connect_to_ui.md b/docs/v2/connect_to_ui.md
new file mode 100644
index 0000000..a44a194
--- /dev/null
+++ b/docs/v2/connect_to_ui.md
@@ -0,0 +1,13 @@
+# Connect to UI
+
+Pydantic can generate JSON schemas that support the OpenAPI specification. This means it can convey information such as types and methods across languages.
+
+If using FastAPI, you can refer to the [documentation](https://fastapi.tiangolo.com/advanced/generate-clients/?h=openapi.json#preprocess-the-openapi-specification-for-the-client-generator) to directly generate a TypeScript frontend SDK.
+
+This allows backend changes to be directly passed to the frontend.
+
+A simple example can be found in the repo: [pydantic-resolve-demo](https://github.com/allmonday/pydantic-resolve-demo)
+
+With the assistance of OpenAPI and SDK generation, the cost of front-end and back-end integration is significantly reduced.
+
+When the backend data structure changes, the frontend can immediately detect the changes with TypeScript upon synchronization.
diff --git a/docs/v2/connect_to_ui.zh.md b/docs/v2/connect_to_ui.zh.md
new file mode 100644
index 0000000..2805390
--- /dev/null
+++ b/docs/v2/connect_to_ui.zh.md
@@ -0,0 +1,13 @@
+# 连接到 UI
+
+pydantic 本身可以生成支持 OpenAPI 规范的 json schema。 这意味着它能够跨语言传递类型和方法等信息。
+
+如果使用 FastAPI， 可以参考文档 [link](https://fastapi.tiangolo.com/advanced/generate-clients/?h=openapi.json#preprocess-the-openapi-specification-for-the-client-generator) 直接生成 typescript 的前端 sdk
+
+这使得后端的改动可以直接传递到前端。
+
+简单样例可以参考 repo： [pydantic-resolve-demo](https://github.com/allmonday/pydantic-resolve-demo)
+
+借助 OpenAPI 和 sdk 生成， 前后端对接的成本直接锐减。
+
+当后端数据结构发生变化之后， 前端同步之后也能第一时间借助 TypeScript 感知到变更。
diff --git a/docs/v2/dataloader.md b/docs/v2/dataloader.md
new file mode 100644
index 0000000..2cad2a1
--- /dev/null
+++ b/docs/v2/dataloader.md
@@ -0,0 +1,251 @@
+# DataLoader
+
+DataLoader is a very important component in pydantic-resolve, utilizing a third-party independent library [aiodataloader](https://github.com/syrusakbary/aiodataloader). It is often used as a dependency in GraphQL-related libraries.
+
+It can solve the N+1 query problem in GraphQL by combining multiple concurrent queries into a single batch query to optimize performance.
+
+The internal mechanism of pydantic-resolve is somewhat similar to GraphQL, so it can be directly used to handle data loading. For some simple DataLoaders, reuse with other Python GraphQL frameworks can also be achieved.
+
+For example, the code introduced in the introduction, when batch processing `get_cars_by_child`, multiple triggers occur, causing the N+1 query problem. We can solve this with DataLoader.
+
+```python
+class Child(BaseModel):
+    id: int
+    name: str
+
+    cars: List[Car] = []
+    # async def resolve_cars(self):
+    #     return await get_cars_by_child(self.id)
+
+    cars: List[Car] = []
+    async def resolve_cars(self, loader=LoaderDepend(CarLoader)):
+        return await loader.load(self.id)
+
+    description: str = ''
+    def post_description(self):
+        desc = ', '.join([c.name for c in self.cars])
+        return f'{self.name} owns {len(self.cars)} cars, they are: {desc}'
+
+children = await Resolver.resolve([
+        Child(id=1, name="Titan"), Child(id=1, name="Siri")])
+```
+
+## Creating a DataLoader
+
+There are two ways to create a DataLoader object. One is by inheritance:
+
+```python
+from aiodataloader import DataLoader
+
+class UserLoader(DataLoader):
+    max_batch_size = 20
+    async def batch_load_fn(self, keys):
+        return await my_batch_get_users(keys)
+
+user_loader = UserLoader()
+```
+
+The inheritance method allows setting some `aiodataloader` related parameters, such as using `max_batch_size = 20` to slice and parallelize the keys.
+
+These parameters are reserved parameter names for aiodataloader, so be careful not to use the same names when adding new parameters.
+
+The other way is to directly create an `async def batch_load_fn(keys)` method, and pydantic-resolve will use `DataLoader(batch_load_fn)` to create an instance internally.
+
+Generally, it is recommended to use the first method, and the reasons will be explained later.
+
+Next, we will introduce a series of features provided by pydantic-resolve based on DataLoader:
+
+## Multi-layer Data Stitching
+
+Take Company, Office, and Member as examples. Office and Member have dataloaders that return their data, as long as the corresponding office_id and member_id are provided.
+
+- OfficeLoader gets the corresponding Office list through company_id
+- MemberLoader gets the Member list through office_id
+
+Taking OfficeLoader as an example, passing in company_ids returns an array of offices obtained in the order of company_ids.
+
+For example, input `company_ids = [1, 2, 3]`, return `[[office_1, office_2], [office_3], [office_4]]`, which means company 1 has `[office_1, office_2]`, and so on.
+
+> build_list is a helper method provided by pydantic-resolve to simplify the assembly process
+
+```python
+class OfficeLoader(DataLoader)
+    async def batch_load_fn(self, company_ids):
+        offices = await get_offices_by_company_ids(company_ids)
+        return build_list(offices, company_ids, lambda x: x['company_id'])  # Assuming the data is a dict
+
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    def resolve_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)  # Returns a Future
+
+class Office(BaseModel):
+    id: int
+    company_id: int
+    name: str
+
+    members: List[Member] = []
+    def resolve_members(self, loader=LoaderDepend(MemberLoader)):
+        return loader.load(self.id)  # Returns a Future
+
+class Member(BaseModel):
+    id: int
+    office_id: int
+    name: str
+
+companies = [
+    Company(id=1, name='Aston'),
+    Compay(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")
+]
+companies = await Resolver().resolve(companies)
+```
+
+In this code, Company extends the offices data, and then Office extends the members data. During the entire process, the batch_load_fn in OfficeLoader and MemberLoader only executes once, regardless of the number of Offices and members.
+
+> Of course, if max_batch_size is relatively small, multiple batch_load_fn may be executed concurrently
+
+## Providing Parameters and Cloning
+
+DataLoader can add parameters, but be careful to avoid conflicts with the default parameters in `aiodataloader`: `batch`, `max_batch_size`, `cache`, `cache_key_fn`, `cache_map`.
+
+For example, add a `status` parameter to OfficeLoader to filter `open` offices.
+
+```python
+class OfficeLoader(DataLoader)
+    status: Literal['open', 'closed', 'inactive']
+
+    async def batch_load_fn(self, company_ids):
+        offices = await get_offices_by_company_ids_by_status(company_ids, self.status)
+        return build_list(offices, company_ids, lambda x: x['company_id'])
+```
+
+The defined parameters can be set in the Resolver through loader_params.
+
+```python
+companies = [
+    Company(id=1, name='Aston'),
+    Compay(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")
+]
+companies = await Resolver(
+    loader_params={
+        OfficeLoader: {
+            'status': 'open'
+        }
+    }
+).resolve(companies)
+```
+
+There is still a problem here. What if you need to get both `open` and `closed` data at the same time?
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    open_offices: List[Office] = []
+    def resolve_open_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)
+
+    closed_offices: List[Office] = []
+    def resolve_closed_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)
+```
+
+Different conditions for the data cannot be provided by a single OfficeLoader. pydantic-resolve provides a tool to clone two OfficeLoaders.
+
+```python
+from pydantic_resolve import copy_dataloader_kls
+
+OfficeLoader1 = copy_dataloader_kls('OfficeLoader1', OfficeLoader)
+OfficeLoader2 = copy_dataloader_kls('OfficeLoader2', OfficeLoader)
+
+class Company(BaseModel):
+    id: int
+    name: str
+
+    open_offices: List[Office] = []
+    def resolve_open_offices(self, loader=LoaderDepend(OfficeLoader1)):
+        return loader.load(self.id)
+
+    closed_offices: List[Office] = []
+    def resolve_closed_offices(self, loader=LoaderDepend(OfficeLoader2)):
+        return loader.load(self.id)
+
+companies = [
+    Company(id=1, name='Aston'),
+    Company(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")
+]
+companies = await Resolver(
+    loader_params={
+        OfficeLoader1: {
+            'status': 'open'
+        }，
+        OfficeLoader2: {
+            'status': 'closed'
+        }
+    }
+).resolve(companies)
+```
+
+## Handling DataLoader Return Values
+
+If you need to process the return value of `loader.load(self.id)`, you can change resolve_offices to async.
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    async def resolve_offices(self, loader=LoaderDepend(OfficeLoader)):
+        offices = await loader.load(self.id)
+        return [of for of in offices if of['status'] == 'open']
+```
+
+In addition, pydantic-resolve does not restrict the name or number of loaders, so the following usage is also allowed. The example given is a bit strange though.
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    async def resolve_offices(
+            self,
+            office_loader=LoaderDepend(OfficeLoader),
+            manager_loader=LoaderDepend(ManagerLoader)):
+        offices = await office_loader.load(self.id)
+        managers = await manager_loader.load(self.id)
+
+        offices = [of for of in offices if of['manager'] in managers]
+        return offices
+```
+
+## Pre-generating DataLoader Instances
+
+You can generate DataLoader instances in advance and set the data in advance, so you can use the cache mechanism of DataLoader to avoid query overhead during the resolve() phase.
+
+This can be helpful in some specific scenarios.
+
+```python
+loader = SomeLoader()
+loader.prime('tangkikodo', ['tom', 'jerry'])
+loader.prime('john', ['mike', 'wallace'])
+data = await Resolver(loader_instances={SomeLoader: loader}).resolve(data)
+```
+
+## Viewing DataLoader Instance Data
+
+If you want to know which instances DataLoader has initialized and what data it has obtained, you can print it out to check.
+
+```python
+resolver = Resolver()
+data = await resolver.resolve(data)
+print(resolver.loader_instance_cache)
+```
diff --git a/docs/v2/dataloader.zh.md b/docs/v2/dataloader.zh.md
new file mode 100644
index 0000000..d64bd7a
--- /dev/null
+++ b/docs/v2/dataloader.zh.md
@@ -0,0 +1,251 @@
+# 数据加载器 （DataLoader）
+
+DataLoader 是 pydantic-resolve 中非常重要的一个组件， 使用了一个第三方独立库 [aiodataloader](https://github.com/syrusakbary/aiodataloader)， 它常常被作为依赖用在 GraphQL 相关的库中。
+
+它可以解决 GraphQL 的 N+1 查询问题， 能将多个并发查询合并之后变成一次 batch 查询来优化性能。
+
+pydantic-resolve 的内部机制和 GraphQL 有点类似， 所以可以直接使用它来负责数据加载。 对于一些简单的 DataLoader 也可以实现与其他 python GraphQL 框架间的复用。
+
+比如简介中介绍的代码, 在批量处理 `get_cars_by_child` 时就会触发多次, 引起 N+1 查询问题. 我们能通过 DataLoader 来解决.
+
+```python
+class Child(BaseModel):
+    id: int
+    name: str
+
+    cars: List[Car] = []
+    # async def resolve_cars(self):
+    #     return await get_cars_by_child(self.id)
+
+    cars: List[Car] = []
+    async def resolve_cars(self, loader=LoaderDepend(CarLoader)):
+        return await loader.load(self.id)
+
+    description: str = ''
+    def post_description(self):
+        desc = ', '.join([c.name for c in self.cars])
+        return f'{self.name} owns {len(self.cars)} cars, they are: {desc}'
+
+children = await Resolver.resolve([
+        Child(id=1, name="Titan"), Child(id=1, name="Siri")])
+```
+
+## DataLoader 的创建
+
+DataLoader 对象有两种创建方法， 一种是继承：
+
+```python
+from aiodataloader import DataLoader
+
+class UserLoader(DataLoader):
+    max_batch_size = 20
+    async def batch_load_fn(self, keys):
+        return await my_batch_get_users(keys)
+
+user_loader = UserLoader()
+```
+
+继承的写法可以设置一些 `aiodataloader` 相关的参数， 比如使用 `max_batch_size = 20` 来对 keys 做切片并发。
+
+这几个 param 作为 aiodataloader 的保留参数名， 在新增 param 时需要注意不要重名。
+
+另一种则是直接创建一个 `async def batch_load_fn(keys)` 方法， 在 pydantic-resolve 内部会使用 `DataLoader(batch_load_fn)` 来创建实例。
+
+一般建议使用第一种方法，原因会在下文阐述。
+
+接下来将介绍 pydantic-resolve 基于 DataLoader 所提供的一些列功能：
+
+## 多层的数据拼接
+
+拿 Company, Office, Member 来举例子， Office 和 Member 有返回他们数据的 dataloader， 只要提供对应的 office_id 和 member_id。
+
+- OfficeLoader 通过 company_id 来获取对应的 Office 列表
+- MemberLoader 通过 office_id 来获取 Member 列表
+
+以 OfficeLoader 为例， 传入 company_ids， 返回的是以 company_ids 为顺序， 获取到的 offices 数组。
+
+比如输入 `company_ids = [1, 2, 3]`, 返回 `[[office_1, office_2], [office_3], [office_4]]`， 代表 company 1 有 `[office_1, office_2]`，以此类推。
+
+> build_list 是 pydantic-resolve 提供的 helper 方法， 来简化拼装过程
+
+```python
+class OfficeLoader(DataLoader)
+    async def batch_load_fn(self, company_ids):
+        offices = await get_offices_by_company_ids(company_ids)
+        return build_list(offices, company_ids, lambda x: x['company_id'])  # 假定数据是 dict
+
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    def resolve_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)  # 返回的是 Future
+
+class Office(BaseModel):
+    id: int
+    company_id: int
+    name: str
+
+    members: List[Member] = []
+    def resolve_members(self, loader=LoaderDepend(MemberLoader)):
+        return loader.load(self.id)  # 返回的是 Future
+
+class Member(BaseModel):
+    id: int
+    office_id: int
+    name: str
+
+companies = [
+    Company(id=1, name='Aston'),
+    Compay(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")
+]
+companies = await Resolver().resolve(companies)
+```
+
+在这段代码中， Company 扩展了 offices 数据， 然后 Office 扩展了 members 数据， 在整个过程中，OfficeLoader 和 MemberLoader 中的 batch_load_fn 都只执行了一次，无论 Offices 和 members 的数量是多少。
+
+> 当然如果 max_batch_size 比较小的话， 可能会并发多个 batch_load_fn
+
+## 提供参数和克隆
+
+DataLoader 可以添加参数， 但需要注意避免和 `aiodataloader` 中的默认参数冲突 `batch`, `max_batch_size`, `cache`, `cache_key_fn`, `cache_map`.
+
+比如给 OfficeLoader 添加 `status` 参数， 用来过滤 `open` 的 office。
+
+```python
+class OfficeLoader(DataLoader)
+    status: Literal['open', 'closed', 'inactive']
+
+    async def batch_load_fn(self, company_ids):
+        offices = await get_offices_by_company_ids_by_status(company_ids, self.status)
+        return build_list(offices, company_ids, lambda x: x['company_id'])
+```
+
+定义好的参数可以在 Resolver 中通过 loader_params 来设置。
+
+```python
+companies = [
+    Company(id=1, name='Aston'),
+    Compay(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")
+]
+companies = await Resolver(
+    loader_params={
+        OfficeLoader: {
+            'status': 'open'
+        }
+    }
+).resolve(companies)
+```
+
+这里还存在一个问题， 如果需要同时获取 `open` 和 `closed` 的数据呢。
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    open_offices: List[Office] = []
+    def resolve_open_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)
+
+    closed_offices: List[Office] = []
+    def resolve_closed_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)
+```
+
+两个不同条件的数据没法使用单个 OfficeLoader 来提供数据， pydantic-resolve 提供了一个工具来克隆两个 OfficeLoader 出来。
+
+```python
+from pydantic_resolve import copy_dataloader_kls
+
+OfficeLoader1 = copy_dataloader_kls('OfficeLoader1', OfficeLoader)
+OfficeLoader2 = copy_dataloader_kls('OfficeLoader2', OfficeLoader)
+
+class Company(BaseModel):
+    id: int
+    name: str
+
+    open_offices: List[Office] = []
+    def resolve_open_offices(self, loader=LoaderDepend(OfficeLoader1)):
+        return loader.load(self.id)
+
+    closed_offices: List[Office] = []
+    def resolve_closed_offices(self, loader=LoaderDepend(OfficeLoader2)):
+        return loader.load(self.id)
+
+companies = [
+    Company(id=1, name='Aston'),
+    Company(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")
+]
+companies = await Resolver(
+    loader_params={
+        OfficeLoader1: {
+            'status': 'open'
+        }，
+        OfficeLoader2: {
+            'status': 'closed'
+        }
+    }
+).resolve(companies)
+```
+
+## 处理 DataLoader 返回值
+
+如果需要对 `loader.load(self.id)` 的返回值做处理， 可以将 resolve_offices 改为 async.
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    async def resolve_offices(self, loader=LoaderDepend(OfficeLoader)):
+        offices = await loader.load(self.id)
+        return [of for of in offices if of['status'] == 'open']
+```
+
+另外， pydantic-resolve 并不限制 loader 的名字， 或者数量， 所以以下的用法也是允许的 ：）, 当然给的例子稍微有点奇怪。。
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    async def resolve_offices(
+            self,
+            office_loader=LoaderDepend(OfficeLoader),
+            manager_loader=LoaderDepend(ManagerLoader)):
+        offices = await office_loader.load(self.id)
+        managers = await manager_loader.load(self.id)
+
+        offices = [of for of in offices if of['manager'] in managers]
+        return offices
+```
+
+## 预先生成 DataLoader 实例
+
+可以提前生成 DataLoader 的实例， 将数据提前设置进去， 这样就能利用 DataLoader 的 cache 机制避免 resolve() 阶段的查询开销。
+
+在一些特定场景会有帮助。
+
+```python
+loader = SomeLoader()
+loader.prime('tangkikodo', ['tom', 'jerry'])
+loader.prime('john', ['mike', 'wallace'])
+data = await Resolver(loader_instances={SomeLoader: loader}).resolve(data)
+```
+
+## 查看 DataLoader 实例数据
+
+如果想知道 DataLoader 初始化了哪些实例, 获取了哪些数据, 可以打印出来查看.
+
+```python
+resolver = Resolver()
+data = await resolver.resolve(data)
+print(resolver.loader_instance_cache)
+```
diff --git a/docs/v2/erd_driven.md b/docs/v2/erd_driven.md
new file mode 100644
index 0000000..9ca5259
--- /dev/null
+++ b/docs/v2/erd_driven.md
@@ -0,0 +1,256 @@
+# ERD-Driven Development
+
+ERD - Entity Relationship Diagram
+
+For backend developers, this is a very familiar concept. Many database tools have ERD visualization features.
+
+ERD itself can be a more abstract concept, independent of specific database implementations. It describes the entities and the relationships between them, so many product managers also use ERD to describe the core data relationships of a product.
+
+Therefore, ERD is an important tool that runs through both product design and implementation. If **the structure of the ERD can remain clear in all stages**, the overall product becomes more maintainable and scalable.
+
+When ERD is combined with pydantic-resolve, it can achieve a 3-5x increase in development efficiency and a 50% reduction in code volume.
+
+Let's start by discussing some existing development methods, their capabilities, and limitations.
+
+```mermaid
+---
+title: User and Post
+---
+
+erDiagram
+    User ||--o{ Post : "has many"
+
+```
+
+## From SQL, ORM, to GraphQL
+
+### SQL
+
+Relational databases can store related data, but it is not convenient to fetch objects and their related objects through SQL.
+
+When using SQL for join queries, if the related table is one-to-many, it will cause an increase in the number of Cartesian products.
+
+Simple queries cannot generate nested related data (although there are some tricks to assemble JSON, the maintainability is poor), or deduplicate parent nodes through the application layer.
+
+```sql
+select * from user join post on user.id = post.user_id
+```
+
+So the result of SQL is a two-dimensional table, and the related data can only be displayed as aggregated results.
+
+```sql
+select user.name, count(*) as post_count from user join post on user.id = post.user_id groupby user.id
+```
+
+### ORM
+
+If you need to fetch related information, you will use ORM. After defining the relationship in ORM, you can get the related objects.
+
+Take the commonly used SQLAlchemy as an example.
+
+```python
+from sqlalchemy import Column, Integer, String, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class User(Base):
+    __tablename__ = 'users'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String)
+
+    # Define relationship
+    posts = relationship("Post", back_populates="user")
+
+class Post(Base):
+    __tablename__ = 'posts'
+
+    id = Column(Integer, primary_key=True)
+    title = Column(String)
+    user_id = Column(Integer, ForeignKey('users.id'))
+
+    # Define relationship
+    user = relationship("User", back_populates="posts")
+
+rows = session.query(User).options(joinedload(User.posts)).all()
+```
+
+For fetching related data, you can adjust through various lazy options. Select will cause N+1 queries when looping through User, while joined and subquery will preload the data. These options need to be adjusted by developers to avoid performance issues.
+
+However, **ORM also has some limitations**. If some data is not in the database, such as data from a third-party API or local files, you cannot enjoy the convenience of automatic association.
+
+### GraphQL
+
+The emergence of GraphQL provides a "new" idea. **Its schema is a way that is easy to approach ERD descriptions**.
+
+In addition, it abstracts the concept of DataLoader, using the general format of `async def batch_load_fn(keys)` to define input parameters and return data. Users can decide the implementation method themselves.
+
+For example, in the case of a database, you can use `where ..in ..` to batch search.
+
+```sql
+select * from post where user_id in (1, 2, 3)
+```
+
+Then aggregate the fetched data in the code with the logic of groupby `post.user_id`.
+
+If it is a third-party API, just make a simple asynchronous call.
+
+```python
+async def batch_load_fn(user_ids):
+    posts = await get_posts_by_user_ids(user_ids)
+    return build_list(posts, user_ids, lambda x: x.user_id)
+```
+
+This mechanism of GraphQL achieves a general interface that is independent of specific implementations, leaving ample room for internal optimization.
+
+However, the power of DataLoader is limited under the GraphQL system.
+
+The most common scenario is that DataLoader can only associate data through keys, which is a single foreign key. If you want to filter resources additionally, it will be difficult.
+
+For example, `(1, 2, 3, 4)` are the keys passed in, and there is no suitable way to set the `where` condition.
+
+```sql
+select * from post where post.user_id in (1,2,3,4)
+    where post.created_at > '2021-12-12'
+```
+
+From the perspective of setting parameters, keys are provided by `loader.load(key)` for each User object, while the `where` condition is directly configured for the loader.
+
+GraphQL itself does not have a convenient way to provide a general `where` parameter, which is a pity.
+
+GraphQL's description of data is already very close to ERD, but unfortunately, this architecture is relatively large, with too many framework constraints, and providing flexible query interfaces is also a double-edged sword.
+
+We hope to find a simpler way.
+
+## Using Pydantic to Define ERD
+
+Pydantic is an excellent candidate. We can use it to define Entity and Relationship.
+
+```python
+class User(BaseModel):
+    id: int
+    name: str
+
+class Post(BaseModel):
+    id: int
+    user_id: int
+    title: str
+
+class PostLoader(DataLoader):
+    async def batch_load_fn(self, user_ids):
+        posts = await get_posts_by_user_ids(user_ids)
+        return build_list(posts, user_ids, lambda x: x.user_id)
+```
+
+Using Pydantic to define the structure of User and Post is very concise and clear. It can be used as an abstract expression independent of the persistence layer.
+
+The association between User and Post is defined by DataLoader. The specific implementation is handled by `get_post_by_user_ids`.
+
+For example, a query like `session.query(UserModel).all()`, or a remote request using `aiohttp`.
+
+> The relationship between User and Post is not limited to being described by only one DataLoader. In fact, multiple DataLoaders can be defined and selected according to the actual scenario.
+
+```mermaid
+---
+title:
+---
+
+erDiagram
+    User ||..o{ Post : "PostLoader"
+    User ||..o{ Post : "AnotherLoader"
+
+```
+
+Use dashed lines to indicate the possible associations between them.
+
+### Establishing Associations
+
+Now let's assume there is a business requirement to establish an association between User and Post.
+
+You can reuse User fields by inheriting User and define `posts` to link the data.
+
+```mermaid
+---
+title: UserWithPostsForSpecificBusiness
+---
+
+erDiagram
+    User ||--o{ Post : "PostLoader"
+
+```
+
+```python
+class UserWithPostsForSpecificBusiness(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+```
+
+In this way, `UserWithPostsForSpecificBusiness` is a fixed combination of User and Post associations established for specific business requirements.
+
+By the way, as mentioned in "Data Loaders", DataLoader allows additional parameters to be provided.
+
+### The Secret to Maintainable Code: Keep Business ERD and Code Structure Definitions Consistent
+
+Now we have obtained a code that is highly consistent with the business requirement ERD structure, and this code is dedicated.
+
+In other words, ERD defines a series of Entities and all possible Relationships, and the actual establishment of relationships depends on the specific business requirements.
+
+Two classes with exactly the same structure can have different names, representing different needs.
+
+```python
+class UserWithPostsForSpecificBusinessA(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+
+class UserWithPostsForSpecificBusinessB(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+```
+
+Suppose the requirement of `UserWithPostsForSpecificBusinessA` changes, and it needs to load only the latest 3 posts for each user.
+
+You only need to create a new DataLoader and replace it. (UserWithPostsForSpecificBusinessB is completely unaffected)
+
+```python
+class UserWithPostsForSpecificBusinessA(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(LatestThreePostLoader)):
+        return loader.load(self.id)
+```
+
+In the end, we achieved the goal of keeping the code structure highly consistent with the product design ERD structure, making subsequent changes and adjustments easier.
+
+### More Examples
+
+```mermaid
+---
+title: Business A ERD
+---
+
+erDiagram
+    User ||--o{ Post : "PostLoader"
+    Post ||--o{ Comment : "CommentLoader"
+    Post ||--o{ Like : "LikeLoader"
+
+```
+
+```python
+class BizAPost(Post):
+    comments: List[Comment] = []
+    def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+        return loader.load(self.id)
+
+    likes: List[Like] = []
+    def resolve_likes(self, loader=LoaderDepend(LikeLoader)):
+        return loader.load(self.id)
+
+class BizAUser(User):
+    posts: List[BizAPost] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+```
diff --git a/docs/v2/erd_driven.zh.md b/docs/v2/erd_driven.zh.md
new file mode 100644
index 0000000..17b9ce3
--- /dev/null
+++ b/docs/v2/erd_driven.zh.md
@@ -0,0 +1,256 @@
+# ERD 驱动开发
+
+ERD - Entity Relationship Diagram 实体关系图
+
+对后端程序员来说这是一个很熟悉的概念， 很多数据库工具都有 ERD 可视化的功能。
+
+ERD 本身可以是一个更加抽象的概念， 可以脱离具体数据库实现。它描述了实体和实体之间的关系， 所以许多产品经理也会用 ERD 来描述产品的核心数据关系。
+
+因此对 ERD 是贯穿产品设计和产品实现的一个重要工具， 如果 **ERD 的结构可以在所有环节中维持清晰**， 那就能让产品整体更加可维护和可扩展。
+
+当 ERD 和 pydantic-resolve 结合在一起， 就能实现 3 - 5 倍开发效率的提升， 以及 50% 代码量的减少。
+
+我们先从已有的一些开发手段讲起， 说说他们的能力和局限。
+
+```mermaid
+---
+title: User and Post
+---
+
+erDiagram
+    User ||--o{ Post : "has many"
+
+```
+
+## 从 SQL，ORM，到 GraphQL
+
+### SQL
+
+关系型数据库可以存储有相关性的数据， 但是通过 SQL 获取对象和关联对象并不方便。
+
+使用 SQL 做 join 查询的时候， 如果关联的是一对多的表，那么就会引起笛卡尔积数量增加的情况。
+
+简单的查询无法生成嵌套的关联数据 （ 虽然有拼装 json 的奇技淫巧， 但那个的可维护性比较糟糕 ）， 或者通过应用层做父节点去重。
+
+```sql
+select * from user join post on user.id = post.user_id
+```
+
+所以 SQL 的结果是一张二维表， 关联的数据只能转换成聚合计算的结果来展示。
+
+```sql
+select user.name, count(*) as post_count from user join post on user.id = post.user_id groupby user.id
+```
+
+### ORM
+
+如果需要获取关联信息的话， 就会使用到 ORM， 在 ORM 中定义 relationship 之后， 就能获取到关联的对象。
+
+以常用的 sqlalchemy 为例。
+
+```python
+from sqlalchemy import Column, Integer, String, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.ext.declarative import declarative_base
+
+Base = declarative_base()
+
+class User(Base):
+    __tablename__ = 'users'
+
+    id = Column(Integer, primary_key=True)
+    name = Column(String)
+
+    # 定义 relationship
+    posts = relationship("Post", back_populates="user")
+
+class Post(Base):
+    __tablename__ = 'posts'
+
+    id = Column(Integer, primary_key=True)
+    title = Column(String)
+    user_id = Column(Integer, ForeignKey('users.id'))
+
+    # 定义 relationship
+    user = relationship("User", back_populates="posts")
+
+rows = session.query(User).options(joinedload(User.posts)).all()
+```
+
+对于关联数据的获取， 可以通过多种 lazy 选项来调整， select 在对 User 循环时会引起 N+1 查询， joined， subquery 则会提前将数据查询加载好。 这些选项的调整是需要开发去关注来避免性能问题的。
+
+但同时 **ORM 也伴随了一些局限**， 如果有些数据不是在数据库中，比如需要从某个第三方 API， 或者本地文件中获取，就没法享受到自动关联的便利了。
+
+### GraphQL
+
+GraphQL 的出现提供了一种 “新的” 思路， **它的 schema 就是一种容易贴近 ERD 描述的方式**。
+
+此外它抽象了 DataLoader 的概念， 使用 `async def batch_loadn_fn(keys)` 的通用格式来定义输入参数和返回数据， 用户可以自己决定实现方式。
+
+以数据库为例， 可以使用 `where ..in ..` 来批量查找。
+
+```sql
+select * from post where user_id in (1, 2, 3)
+```
+
+然后将获取的数据在代码中 groupby `post.user_id` 的逻辑做聚合。
+
+如果是第三方 API 的话， 只要简单发起一次异步调用。
+
+```python
+async def batch_load_fn(user_ids):
+    posts = await get_posts_by_user_ids(user_ids)
+    return build_list(posts, user_ids, lambda x: x.user_id)
+```
+
+GraphQL 的这个机制实现了这样一种和具体实现无关的通用接口， 这也为内部优化预留了充足的空间。
+
+但 DataLoader 的威力在 GraphQL 体系下是被限制了的。
+
+最常见的场景是 DataLoader 默认只能通过 keys， 也就是单一的外键来关联数据， 如果要对资源做额外过滤， 会很难做。
+
+用查询来举例， `(1, 2, 3, 4)` 是传入的 keys， `where` 条件则没有合适的手段来设置。
+
+```sql
+select * from post where post.user_id in (1,2,3,4)
+    where post.created_at > '2021-12-12'
+```
+
+从设置参数的角度来看， keys 是一个个 User 对象通过 `loader.load(key)` 提供的， 而 `where` 条件是则是直接针对 loader 做的配置。
+
+GraphQL 本身没有方便的手段来提供通用的 `where` 参数， 这点比较遗憾。
+
+GraphQL 对数据的描述已经非常贴近 ERD 了，可惜这个架构比较庞大， 存在太多框架的约束， 提供灵活的查询接口也是一把双刃剑。
+
+我们希望找到一个更加简洁的方式。
+
+## 使用 Pydantic 来定义 ERD
+
+pydantic 就是一个优秀的候选人， 我们可以用它来定义 Entity 和 Relationship。
+
+```python
+class User(BaseModel):
+    id: int
+    name: str
+
+class Post(BaseModel):
+    id: int
+    user_id: int
+    title: str
+
+class PostLoader(DataLoader):
+    async def batch_load_fn(self, user_ids):
+        posts = await get_posts_by_user_ids(user_ids)
+        return build_list(posts, user_ids, lambda x: x.user_id)
+```
+
+使用 pydantic 来定义 User， Post 的结构， 非常简洁清晰。 可以作为脱离持久层的抽象表达。
+
+而 User 和 Post 的关联由 DataLoader 来定义。 具体的实现交由 `get_post_by_user_ids` 来负责实现。
+
+比如一个 `session.query(UserModel).all()` 的查询， 或者 `aiohttp` 的远程请求。
+
+> Usr 和 Post 的关系并未限定只能有一种 DataLoader 来描述，实际上可以定义多种 DataLoader 根据实际场景来选用。
+
+```mermaid
+---
+title:
+---
+
+erDiagram
+    User ||..o{ Post : "PostLoader"
+    User ||..o{ Post : "AnotherLoader"
+
+```
+
+使用虚线来表示他们之间 “可以” 发生的关联。
+
+### 建立关联
+
+现在我们假设有个业务需求， 要给 User 和 Post 建立关联。
+
+可以通过继承 User 来复用 User 字段， 通过定义 `posts` 来联系数据。
+
+```mermaid
+---
+title: UserWithPostsForSpecificBusiness
+---
+
+erDiagram
+    User ||--o{ Post : "PostLoader"
+
+```
+
+```python
+class UserWithPostsForSpecificBusiness(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+```
+
+这样 `UserWithPostsForSpecificBusiness` 就是面向特定业务需求所建立的 User 和 Post 关联固定组合体。
+
+顺带一提， 就如 "数据加载器" 中所提到的， DataLoader 是允许提供额外参数的。
+
+### 可维护代码的诀窍： 使业务 ERD 和代码中的结构定义维持一致
+
+现在我们得到了一个业务需求 ERD 结构高度一致的代码， 并且这段代码是专供的。
+
+也即是说， ERD 定义了一系列 Entity 和所有可能的 Relationship， 而关系的真正建立是取决于实际业务需求的。
+
+两个结构完全一样的 class， 可以拥有不相同的名字， 代表服务于不同的需求。
+
+```python
+class UserWithPostsForSpecificBusinessA(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+
+class UserWithPostsForSpecificBusinessB(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+```
+
+假设 `UserWithPostsForSpecificBusinessA` 的需求发生了变更， 需要只加载每个 user 最近的 3 条 posts
+
+那只需要创建好新的 DataLoader 然后替换进去即可。( UserWithPostsForSpecificBusinessB 则完全不受影响 )
+
+```python
+class UserWithPostsForSpecificBusinessA(User):
+    posts: List[Post] = []
+    def resolve_posts(self, loader=LoaderDepend(LatestThreePostLoader)):
+        return loader.load(self.id)
+```
+
+最终， 我们实现了目标， 让代码侧的结构与产品设计侧的 ERD 结构保持高度的一致， 这使得后续的变更和调整变得更容易。
+
+### 更多案例
+
+```mermaid
+---
+title: Business A ERD
+---
+
+erDiagram
+    User ||--o{ Post : "PostLoader"
+    Post ||--o{ Comment : "CommentLoader"
+    Post ||--o{ Like : "LikeLoader"
+
+```
+
+```python
+class BizAPost(Post):
+    comments: List[Comment] = []
+    def resolve_comments(self, loader=LoaderDepend(CommentLoader)):
+        return loader.load(self.id)
+
+    likes: List[Like] = []
+    def resolve_likes(self, loader=LoaderDepend(LikeLoader)):
+        return loader.load(self.id)
+
+class BizAUser(User):
+    posts: List[BizAPost] = []
+    def resolve_posts(self, loader=LoaderDepend(PostLoader)):
+        return loader.load(self.id)
+```
diff --git a/docs/v2/expose_and_collect.md b/docs/v2/expose_and_collect.md
new file mode 100644
index 0000000..8569e88
--- /dev/null
+++ b/docs/v2/expose_and_collect.md
@@ -0,0 +1,143 @@
+# Expose and Collect
+
+This set of advanced features provided by pydantic-resolve aims to solve the problem of data transmission and assembly across multiple layers, facilitating computations in scenarios with deeply nested structures.
+
+## Providing Data to All Descendant Nodes
+
+The `Company` type has three layers. Add an `introduction` field to the `Employee` type, containing `company - department - employee name`.
+
+```python hl_lines="2 9 21 22"
+class Company(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'company_name' }
+    id: int
+    name: str
+    departments: List[Department]
+
+
+class Department(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'department_name' }
+    id: int
+    name: str
+    employees: List[Employee]
+
+
+class Employee(BaseModel):
+    id: int
+    name: str
+
+    introduction: str = ''
+    def resolve_introduction(self, ancestor_context):
+        company = ancestor_context['company_name']
+        department = ancestor_context['department_name']
+        return f'{company}/{department}/{self.name}'
+
+```
+
+`__pydantic_resolve_expose__` will convert the key in the definition to the value (alias) and then provide it to all descendant nodes.
+
+The `name` in `Company` is converted to `company_name`, and descendant nodes get the data from `ancestor_context['company_name']`.
+
+Note that the alias should be globally unique within the entire structure.
+
+## Collecting Data from Descendant Nodes
+
+Now suppose there is a `Report` class that has a `company` data, and then uses `complyees` to collect all `Employee`'s `introduction` fields.
+
+```python hl_lines="7 26"
+from pydantic_resolve import Collect
+
+class Report(BaseModel):
+    companies: List[Company]
+
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+
+
+class Company(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'company_name' }
+    id: int
+    name: str
+    departments: List[Department]
+
+
+class Department(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'department_name' }
+    id: int
+    name: str
+    employees: List[Employee]
+
+
+class Employee(BaseModel):
+    __pydantic_resolve_collect__ = {'introduction': 'reporter'}
+    id: int
+    name: str
+
+    introduction: str = ''
+    def resolve_introduction(self, ancestor_context):
+        company = ancestor_context['company_name']
+        department = ancestor_context['department_name']
+        return f'{company}/{department}/{self.name}'
+```
+
+`__pydantic_resolve_collect__ = {'introduction': 'reporter'}` means that the value of the `introduction` field will be sent to the `reporter` collector at the end of the node's lifecycle.
+
+Note that the collector needs to be defined in the post method because it must wait for all descendant nodes to complete their calculations before collecting data.
+
+```python
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+```
+
+Like DataLoader, the collector parameter names and quantities are not limited.
+
+```python
+    employees: List[str] = []
+    def post_employees(self, xxx=Collector('xxx'), yyy=Collector('yyy')):
+        return xxx.values() + yyy.values()
+```
+
+Unlike the global uniqueness requirement for aliases in Expose, the collector's name can be defined at multiple levels. The same name collector can collect data of different scales based on the range of descendants.
+
+For example, `Company` can also define `employees` and use the same name collector as in `Report` to collect data at the `Company` level.
+
+```python hl_lines="7 18"
+from pydantic_resolve import Collect
+
+class Report(BaseModel):
+    companies: List[Company]
+
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+
+
+class Company(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'company_name' }
+    id: int
+    name: str
+    departments: List[Department]
+
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+
+class Department(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'department_name' }
+    id: int
+    name: str
+    employees: List[Employee]
+
+
+class Employee(BaseModel):
+    __pydantic_resolve_collect__ = {'introduction': 'reporter'}
+    id: int
+    name: str
+
+    introduction: str = ''
+    def resolve_introduction(self, ancestor_context):
+        company = ancestor_context['company_name']
+        department = ancestor_context['department_name']
+        return f'{company}/{department}/{self.name}'
+```
diff --git a/docs/v2/expose_and_collect.zh.md b/docs/v2/expose_and_collect.zh.md
new file mode 100644
index 0000000..f87a9b1
--- /dev/null
+++ b/docs/v2/expose_and_collect.zh.md
@@ -0,0 +1,143 @@
+# 提供和收集 (expose and collect)
+
+这是 pydantic-resolve 提供的一组高级功能。 它要解决的问题是跨多层的数据传递和组装问题，为一些层级较多的场景的计算提供便利。
+
+## 向所有子孙节点提供数据
+
+Company 类型的数据有三层， 为 Employee 类型添加 introduction 字段， 内容为 company - department - employee name。
+
+```python hl_lines="2 9 21 22"
+class Company(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'company_name' }
+    id: int
+    name: str
+    departments: List[Department]
+
+
+class Department(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'department_name' }
+    id: int
+    name: str
+    employees: List[Employee]
+
+
+class Employee(BaseModel):
+    id: int
+    name: str
+
+    introduction: str = ''
+    def resolve_introduction(self, ancestor_context):
+        company = ancestor_context['company_name']
+        department = ancestor_context['department_name']
+        return f'{company}/{department}/{self.name}'
+
+```
+
+`__pydantic_resolve_expose__` 会将定义中的 key 转变为 value (别名)， 然后提供给所有子孙节点。
+
+Company 中的 name 被转换为 `company_name`， 子孙节点从 `ancestor_context['company_name']` 获取数据。
+
+需要注意别名应该是在整个结构中全局唯一的。
+
+## 从子孙节点收集数据
+
+现在假设有个 Resport 类， 它拥有一个 company 数据， 然后用 complyees 收集所有 Employee 的 introduction 字段
+
+```python hl_lines="7 26"
+from pydantic_resolve import Collect
+
+class Report(BaseModel):
+    companies: List[Company]
+
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+
+
+class Company(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'company_name' }
+    id: int
+    name: str
+    departments: List[Department]
+
+
+class Department(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'department_name' }
+    id: int
+    name: str
+    employees: List[Employee]
+
+
+class Employee(BaseModel):
+    __pydantic_resolve_collect__ = {'introduction': 'reporter'}
+    id: int
+    name: str
+
+    introduction: str = ''
+    def resolve_introduction(self, ancestor_context):
+        company = ancestor_context['company_name']
+        department = ancestor_context['department_name']
+        return f'{company}/{department}/{self.name}'
+```
+
+`__pydantic_resolve_collect__ = {'introduction': 'reporter'}` 的意思是将 introduction 字段的值， 在该节点的声明周期结束时， 发送给 `reporter` 收集者。
+
+注意 collector 需要定义在 post 方法中， 因为必须等待子孙节点的运算都完成之后才可以收集数据。
+
+```python
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+```
+
+和 DataLoader 一样， collecotr 参数名字和数量没有限制。
+
+```python
+    employees: List[str] = []
+    def post_employees(self, xxx=Collector('xxx'), yyy=Collector('yyy')):
+        return xxx.values() + yyy.values()
+```
+
+和 Expose 中限制别名全局唯一不同， collector 的名字可以定义在多层之中。 代表的含义相同名字的收集者可以根据子孙范围收集到不同规模的数据。
+
+比如 Company 中也能定义 employees，使用和 Report 中相同名字的收集器来收集 Company 级别的数据。
+
+```python hl_lines="7 18"
+from pydantic_resolve import Collect
+
+class Report(BaseModel):
+    companies: List[Company]
+
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+
+
+class Company(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'company_name' }
+    id: int
+    name: str
+    departments: List[Department]
+
+    employees: List[str] = []
+    def post_employees(self, collector=Collector('reporter')):
+        return collector.values()
+
+class Department(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'department_name' }
+    id: int
+    name: str
+    employees: List[Employee]
+
+
+class Employee(BaseModel):
+    __pydantic_resolve_collect__ = {'introduction': 'reporter'}
+    id: int
+    name: str
+
+    introduction: str = ''
+    def resolve_introduction(self, ancestor_context):
+        company = ancestor_context['company_name']
+        department = ancestor_context['department_name']
+        return f'{company}/{department}/{self.name}'
+```
diff --git a/docs/v2/inherit_reuse.md b/docs/v2/inherit_reuse.md
new file mode 100644
index 0000000..50a239a
--- /dev/null
+++ b/docs/v2/inherit_reuse.md
@@ -0,0 +1,51 @@
+# Inheritance and Reuse
+
+If a query method or DataLoader can return data of type `Car`, then a subclass `CarWithRepairHistories` that inherits from `Car` can directly replace `Car` in any scenario.
+
+After resolving, the data under `CarWithRepairHistories` will be loaded.
+
+> The prerequisite is that the fields extended by the subclass all have default values.
+
+For example:
+
+```python
+class Car(BaseModel):
+    id: int
+    name: str
+
+def query_car():
+    return dict(id=1, name='Ford')
+
+class ReturnData(BaseModel):
+    car: Optional[Car] = None
+    def resolve_car(self):
+        return query_car()
+```
+
+You can directly replace `Car` with `CarWithRepairHistories` without modifying `query_car` and other query methods.
+
+```python
+class Car(BaseModel):
+    id: int
+    name: str
+
+def query_car():
+    return dict(id=1, name='Ford')
+
+class CarWithRepairHistories(A):
+    histories: List[RepairHistory] = []  # Default value
+    def resolve_histories(self, loader=LoaderDepend(RepairHistoriesLoader)):
+        return loader.load(self.id)
+
+class ReturnData(BaseModel):
+    # car: Optional[Car] = None
+    car: Optional[CarWithRepairHistories] = None
+    def resolve_car(self):
+        return query_a()  # No adjustment needed
+```
+
+Therefore, the methods for obtaining data or DataLoader can be reused to provide data for various subclasses.
+
+Subclass inheritance can reuse the fields of the parent class, reducing the overhead of repeatedly defining fields.
+
+By making good use of inheritance, we can lay the groundwork for the efficient development model of "ERD-oriented development".
diff --git a/docs/v2/inherit_reuse.zh.md b/docs/v2/inherit_reuse.zh.md
new file mode 100644
index 0000000..d354d7b
--- /dev/null
+++ b/docs/v2/inherit_reuse.zh.md
@@ -0,0 +1,51 @@
+# 继承和复用
+
+如果一个 query 方法， 或者 DataLoader 可以返回类型 Car 的数据， 那么继承了 Car 的子类 CarWithRepairHistories， 可以在任意场景直接替换 Car
+
+在 resolve 之后, CarWithRepairHistories 下的数据就会被加载进去
+
+> 前提是子类扩展的字段都有默认值。
+
+比如：
+
+```python
+class Car(BaseModel):
+    id: int
+    name: str
+
+def query_car():
+    return dict(id=1, name='Ford')
+
+class ReturnData(BaseModel):
+    car: Optional[Car] = None
+    def resolve_car(self):
+        return query_car()
+```
+
+就可以用 CarWithRepairHistories 直接替换 Car， 而无需修改 `query_car` 等查询方法。
+
+```python
+class Car(BaseModel):
+    id: int
+    name: str
+
+def query_car():
+    return dict(id=1, name='Ford')
+
+class CarWithRepairHistories(A):
+    histories: List[RepairHistory] = []  # 默认值
+    def resolve_histories(self, loader=LoaderDepend(RepairHistoriesLoader)):
+        return loader.load(self.id)
+
+class ReturnData(BaseModel):
+    # car: Optional[Car] = None
+    car: Optional[CarWithRepairHistories] = None
+    def resolve_car(self):
+        return query_a()  # 无需调整
+```
+
+因此获取数据的方法,或者 DataLoader 都能被复用, 为各种子类提供数据.
+
+子类继承能够复用父类的字段, 减少重复定义字段的开销. 
+
+利用好继承我们就能为 "面向 ERD 开发" 这个高效的开发模式做好铺垫.
\ No newline at end of file
diff --git a/docs/v2/introduction.md b/docs/v2/introduction.md
new file mode 100644
index 0000000..c3603bf
--- /dev/null
+++ b/docs/v2/introduction.md
@@ -0,0 +1,41 @@
+# Introduction
+
+pydantic-resolve is a lightweight wrapper library based on pydantic, which can greatly simplify the complexity of building data.
+
+It can provide you with a 3 to 5 times improvement in development efficiency and reduce more than 50% of the code.
+
+It offers `resolve` and `post` methods for pydantic objects.
+
+```python
+class Child(BaseModel):
+    id: int
+    name: str
+
+    cars: List[Car] = []
+    async def resolve_cars(self):
+        return await get_cars_by_child(self.id)
+
+    description: str = ''
+    def post_description(self):
+        desc = ', '.join([c.name for c in self.cars])
+        return f'{self.name} owns {len(self.cars)} cars, they are: {desc}'
+
+children = await Resolver.resolve([
+        Child(id=1, name="Titan"), Child(id=1, name="Siri")])
+```
+
+`resolve` is usually used to fetch data, while `post` can perform additional processing after fetching the data.
+
+After defining the object methods and initializing the objects, pydantic-resolve will internally traverse the data and execute these methods to process the data.
+
+With the help of dataloader, pydantic-resolve can avoid the N+1 query problem that often occurs when fetching data in multiple layers, optimizing performance.
+
+In addition, it also provides `expose` and `collector` mechanisms to facilitate cross-layer data processing.
+
+## Installation
+
+```
+pip install pydantic-resolve
+```
+
+Starting from pydantic-resolve v1.11.0, it will be compatible with both pydantic v1 and v2.
diff --git a/docs/v2/introduction.zh.md b/docs/v2/introduction.zh.md
new file mode 100644
index 0000000..9e3d6a3
--- /dev/null
+++ b/docs/v2/introduction.zh.md
@@ -0,0 +1,41 @@
+# 简介
+
+pydantic-resolve 是一个基于 pydantic 的轻量级封装库， 可以大幅简化构建数据的复杂度。
+
+它可以为你提供 3 ~ 5 倍的开发效率提升， 减少 50% 以上的代码量。
+
+它为 pydantic 对象提供了 resolve 和 post 方法。
+
+```python
+class Child(BaseModel):
+    id: int
+    name: str
+
+    cars: List[Car] = []
+    async def resolve_cars(self):
+        return await get_cars_by_child(self.id)
+
+    description: str = ''
+    def post_description(self):
+        desc = ', '.join([c.name for c in self.cars])
+        return f'{self.name} owns {len(self.cars)} cars, they are: {desc}'
+
+children = await Resolver.resolve([
+        Child(id=1, name="Titan"), Child(id=1, name="Siri")])
+```
+
+resolve 通常用来获取数据， post 可以在获取数据后做额外处理。
+
+当定义完对象方法， 并初始化好对象后， pydantic-resolve 内部会对数据做遍历， 执行这些方法来处理数据。
+
+借助 dataloader， pydantic-resolve 可以避免多层获取数据时容易发生的 N+1 查询， 优化性能。
+
+除此以外它还提供了 expose 和 collector 机制为跨层的数据处理提供了便利。
+
+## 安装
+
+```
+pip install pydantic-resolve
+```
+
+从 pydantic-resolve v1.11.0 开始， 将同时兼容 pydantic v1 和 v2。
diff --git a/docs/v2/schema_first.md b/docs/v2/schema_first.md
new file mode 100644
index 0000000..73fb59a
--- /dev/null
+++ b/docs/v2/schema_first.md
@@ -0,0 +1,131 @@
+# Define the Model First, Then Describe the Retrieval Method
+
+Using pydantic-resolve's recommended approach, first define the model. Attach all operations to the defined data object.
+
+When constructing data, a straightforward way to think is: first define the target data structure Target, compare it with the existing data source structure Source, and find the shortest path of changes.
+
+> More detailed content will be introduced in "ERD Driven Development".
+
+For example, I have an array of Persons, and now I want to add leave information for each person. The existing leave data in the database includes annual leave and sick leave. These need to be combined into one type.
+
+## Expected Structure
+
+The expected data structure includes `Person` and the corresponding `absenses`. `Absense` is a business-oriented data structure.
+
+```python
+class Person(BaseModel):
+    id: int
+    name: str
+    absenses: List[Absense]
+
+class Absense(BaseModel):
+    type: Literal['annual', 'sick']
+    start_date: date
+    end_date: date
+```
+
+## Adding Data Sources
+
+AnnualLeave and SickLeave are existing data types.
+
+They are provided by AnnualLeaveLoader and SickLeaveLoader respectively.
+
+```python
+class AnnualLeave(BaseModel):
+    person_id: int
+    start_date: date
+    end_date: date
+
+class SickLeave(BaseModel):
+    person_id: int
+    start_date: date
+    end_date: date
+```
+
+At this point, we can combine the source data and the expected data, listing the data to be retrieved and the results to be calculated.
+
+This represents the starting data and the target data.
+
+Using `exclude=True` can hide temporary variables in the final output.
+
+```python
+@model_config()
+class Person(BaseModel):
+    id: int
+    name: str
+
+    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
+    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
+
+    absenses: List[Absense] = []
+```
+
+## Loading Data
+
+Then we add resolve and dataloader for `annual_leaves` and `sick_leaves`, which will provide data for the starting data.
+
+```python
+@model_config()
+class Person(BaseModel):
+    id: int
+    name: str
+
+    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
+    def resolve_annual_leaves(self, loader=LoaderDepend(AnnualLeaveLoader)):
+        return loader.load(self.id)
+
+    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
+    def resolve_sick_leaves(self, loader=LoaderDepend(SickLeaveLoader)):
+        return loader.load(self.id)
+
+    absense: List[Absense] = []
+```
+
+## Transforming Data
+
+When the resolve phase of `annual_leaves` and `sick_leaves` ends, they already have actual data. Next, use the post phase to calculate `absenses`.
+
+```python
+@model_config()
+class Person(BaseModel):
+    id: int
+    name: str
+
+    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
+    def resolve_annual_leaves(self, loader=LoaderDepend(AnnualLeaveLoader)):
+        return loader.load(self.id)
+
+    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
+    def resolve_sick_leaves(self, loader=LoaderDepend(SickLeaveLoader)):
+        return loader.load(self.id)
+
+    absenses: List[Absense] = []
+    def post_absense(self):
+        a = [Absense(
+                type='annual',
+                start_date=a.start_date,
+                end_date=a.end_date) for a in self.annual_leaves]
+        b = [Absense(
+                type='sick',
+                start_date=s.start_date,
+                end_date=s.end_date) for a in self.sick_leaves]
+        return a + b
+```
+
+## Starting the Whole Process
+
+After defining the complete structure of `Person` and `Absense`, we can load and calculate all data by initializing `people`. Just execute `Resolver().resolve()` on the `people` data once to complete all operations.
+
+```python
+people = [Person(id=1, name='alice'), Person(id=2, name='bob')]
+people = await Resolver().resolve(people)
+```
+
+Throughout this entire assembly process, we did not write a single line of code to expand the `people` loop. All related code is inside the `Person` object.
+
+If we were to handle this in a procedural way, we would need to at least consider:
+
+- Aggregating annual and sick leaves based on `person_id`, generating two new variables `person_annual_map` and `person_sick_map`
+- Iterating over `person` objects with `for person in people`
+
+These processes are all encapsulated internally in pydantic-resolve.
diff --git a/docs/v2/schema_first.zh.md b/docs/v2/schema_first.zh.md
new file mode 100644
index 0000000..9167b5a
--- /dev/null
+++ b/docs/v2/schema_first.zh.md
@@ -0,0 +1,131 @@
+# 先定义模型，再描述获取方式
+
+使用 pydantic-resolve 的推荐方式是先定义模型。 将所有的操作都附加在定义好的数据对象上。
+
+当我们构建数据时， 一个很朴素的思考方式是：先定义好目标数据结构 Target， 比较当前已有的数据源结构 Source ，寻找最短的变化路径。
+
+> 更加详细的内容会在 "ERD 驱动开发" 中介绍。
+
+比如我有一个 Person 的数组， 现在要为每个人增加请假信息， 数据库中已有的请假数据包含年假，病假两种， 要把两者合并成一个类型。
+
+## 期望的结构
+
+期望的数据结构就是 Person 和对应的 absenses, Absense 是面向业务的数据结构。
+
+```python
+class Person(BaseModel):
+    id: int
+    name: str
+    absenses: List[Absense]
+
+class Absense(BaseModel):
+    type: Literal['annual', 'sick']
+    start_date: date
+    end_date: date
+```
+
+## 添加数据源
+
+AnnualLeave 和 SickLeave 是已有的数据类型。
+
+分别由 AnnualLeaveLoader 和 SickLeaveLoader 提供具体数据。
+
+```python
+class AnnualLeave(BaseModel):
+    person_id: int
+    start_date: date
+    end_date: date
+
+class SickLeave(BaseModel):
+    person_id: int
+    start_date: date
+    end_date: date
+```
+
+此时我们可以把源数据和期望数据组合在一起, 列出好需要获取的数据和将要计算出来的结果。
+
+这代表了起始数据和目标数据。
+
+配合 `exclude=True` 可以在最终输出中隐藏临时变量。
+
+```python
+@model_config()
+class Person(BaseModel):
+    id: int
+    name: str
+
+    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
+    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
+
+    absenses: List[Absense] = []
+```
+
+## 加载数据
+
+然后我们为 `annual_leaves` 和 `sick_leaves` 添加 resolve 和 dataloader， 他们将为起始数据提供数据。
+
+```python
+@model_config()
+class Person(BaseModel):
+    id: int
+    name: str
+
+    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
+    def resolve_annual_leaves(self, loader=LoaderDepend(AnnualLeaveLoader)):
+        return loader.load(self.id)
+
+    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
+    def resolve_sick_leaves(self, loader=LoaderDepend(SickLeaveLoader)):
+        return loader.load(self.id)
+
+    absense: List[Absense] = []
+```
+
+## 转换数据
+
+当 `annual_leaves` 和 `sick_leaves` 的 resolve 阶段结束时， 他们已经有实际数据了， 接下来利用 post 阶段来计算 absenses
+
+```python
+@model_config()
+class Person(BaseModel):
+    id: int
+    name: str
+
+    annual_leaves: List[AnnualLeave] = Field(default_factory=list, exclude=True)
+    def resolve_annual_leaves(self, loader=LoaderDepend(AnnualLeaveLoader)):
+        return loader.load(self.id)
+
+    sick_leaves: List[SickLeave] = Field(default_factory=list, exclude=True)
+    def resolve_sick_leaves(self, loader=LoaderDepend(SickLeaveLoader)):
+        return loader.load(self.id)
+
+    absenses: List[Absense] = []
+    def post_absense(self):
+        a = [Absense(
+                type='annual',
+                start_date=a.start_date,
+                end_date=a.end_date) for a in self.annual_leaves]
+        b = [Absense(
+                type='sick',
+                start_date=s.start_date,
+                end_date=s.end_date) for a in self.sick_leaves]
+        return a + b
+```
+
+## 启动整个过程
+
+当定义完整个 Person, Absense 的结构后， 我们就能通过初始化 people 来加载和计算所有数据了，只需对 people 数据执行一次 Resolver().resolve() 就能完成所有的操作。
+
+```python
+people = [Person(id=1, name='alice'), Person(id=2, name='bob')]
+people = await Resolver().resolve(people)
+```
+
+在这整个拼装过程中， 我们没有书写一行循环展开 people 的代码，所有的相关代码都在 Person 对象的内部。
+
+如果使用面向过程的方式来处理的话， 会需要至少考虑：
+
+- annual, sick leaves 根据 person_id 聚合，生成 person_annual_map 和 person_sick_map 两个新变量
+- `for person in people` 遍历 person 对象
+
+这些过程在 pydantic-resolve 都是内部封装掉了的。
diff --git a/docs/v2/use_case.md b/docs/v2/use_case.md
new file mode 100644
index 0000000..1f77f60
--- /dev/null
+++ b/docs/v2/use_case.md
@@ -0,0 +1,114 @@
+# Use Cases
+
+Some scenarios where pydantic-resolve can be used.
+
+## Simple Data Concatenation
+
+Concatenate data from multiple data sources (requests at the same level will be automatically concurrent).
+
+```python
+from pydantic_resolve import Resolver
+
+class ReturnData(BaseModel):
+    data: List[str] = []
+    async def resolve_data(self):
+        return await get_data()
+
+    records: List[Record] = []
+    async def resolve_records(self):
+        return await get_records()
+
+retData = ReturnData()
+retData = await Resolver().resolve(retData)
+```
+
+## Multi-layer Data Loading
+
+Use dataloader to concatenate multi-layer data. First, provide the root data, and then let pydantic-resolve concatenate the data of other levels.
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    def resolve_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)
+
+class Office(BaseModel):
+    id: int
+    company_id: int
+    name: str
+
+    members: List[Member] = []
+    def resolve_members(self, loader=LoaderDepend(MemberLoader)):
+        return loader.load(self.id)
+
+class Member(BaseModel):
+    id: int
+    office_id: int
+    name: str
+
+companies = [
+    Company(id=1, name='Aston'),
+    Compay(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")]
+companies = await Resolver().resolve(companies)
+```
+
+## Processing Retrieved Data
+
+You can process data at any position of the object without worrying about the traversal logic.
+
+```python
+class Owner(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'owner_name' }
+    name: str
+    items: List[Item]
+
+class Item(BaseModel):
+    name: str
+
+    description: str = ''
+    def resolve_description(self, ancestor_context):
+        return f'this is item: {self.name}, it belongs to {ancestor_context['owner_name']}'
+
+owners = [
+    dict(name="alice", items=[dict(name='car'), dict(name="house")]),
+    dict(name="bob", items=[dict(name='shoe'), dict(name="pen")]),
+]
+
+owners = await Resolver.resolve([Owner.parse_obj(o) for o in owners])
+```
+
+## Tree Data Processing
+
+Handle self-referential data types more elegantly.
+
+For example, use the parent parameter to concatenate the full path of the tag.
+
+```python
+class Tag(BaseModel):
+    name: str
+
+    full_path: str = ''
+    def resolve_full_path(self, parent):
+        if parent:
+            return f'{parent.full_path}/{self.name}'
+        else:
+            return self.name
+
+    children: List[Tag] = []
+
+
+tag_data = dict(name='root', children=[
+        dict(name='a', children=[
+            dict(name='b', children=[
+                dict(name='e', chidrent=[])
+            ])
+        ])
+    ])
+
+tag = Tag.parse_obj(tag_data)
+tag = await Resolver().resolve(tag)
+```
diff --git a/docs/v2/use_case.zh.md b/docs/v2/use_case.zh.md
new file mode 100644
index 0000000..fa17ac7
--- /dev/null
+++ b/docs/v2/use_case.zh.md
@@ -0,0 +1,114 @@
+# 应用场景
+
+一些 pydantic-resolve 可以使用的场景。
+
+## 简单数据拼接
+
+从多个数据源拼接数据 （同层的请求会自动并发）。
+
+```python
+from pydantic_resolve import Resolver
+
+class ReturnData(BaseModel):
+    data: List[str] = []
+    async def resolve_data(self):
+        return await get_data()
+
+    records: List[Record] = []
+    async def resolve_records(self):
+        return await get_records()
+
+retData = ReturnData()
+retData = await Resolver().resolve(retData)
+```
+
+## 多层数据加载
+
+使用 dataloader 拼接多层数据， 首先提供根数据， 然后让 pydantic-resolve 拼接其他层级的数据。
+
+```python
+class Company(BaseModel):
+    id: int
+    name: str
+
+    offices: List[Office] = []
+    def resolve_offices(self, loader=LoaderDepend(OfficeLoader)):
+        return loader.load(self.id)
+
+class Office(BaseModel):
+    id: int
+    company_id: int
+    name: str
+
+    members: List[Member] = []
+    def resolve_members(self, loader=LoaderDepend(MemberLoader)):
+        return loader.load(self.id)
+
+class Member(BaseModel):
+    id: int
+    office_id: int
+    name: str
+
+companies = [
+    Company(id=1, name='Aston'),
+    Compay(id=2, name="Nicc"),
+    Company(id=3, name="Carxx")]
+companies = await Resolver().resolve(companies)
+```
+
+## 对获取的数据做处理
+
+可以对任意位置的对象做数据处理， 不用关心遍历逻辑。
+
+```python
+class Owner(BaseModel):
+    __pydantic_resolve_expose__ = { 'name': 'owner_name' }
+    name: str
+    items: List[Item]
+
+class Item(BaseModel):
+    name: str
+
+    description: str = ''
+    def resolve_description(self, ancestor_context):
+        return f'this is item: {self.name}, it belongs to {ancestor_context['owner_name']}'
+
+owners = [
+    dict(name="alice", items=[dict(name='car'), dict(name="house")]),
+    dict(name="bob", items=[dict(name='shoe'), dict(name="pen")]),
+]
+
+owners = await Resolver.resolve([Owner.parse_obj(o) for o in owners])
+```
+
+## 树状数据处理
+
+更加优雅地处理自引用类型地数据。
+
+比如使用 parent 参数拼接出 tag 的完整路径。
+
+```python
+class Tag(BaseModel):
+    name: str
+
+    full_path: str = ''
+    def resolve_full_path(self, parent):
+        if parent:
+            return f'{parent.full_path}/{self.name}'
+        else:
+            return self.name
+
+    children: List[Tag] = []
+
+
+tag_data = dict(name='root', children=[
+        dict(name='a', children=[
+            dict(name='b', children=[
+                dict(name='e', chidrent=[])
+            ])
+        ])
+    ])
+
+tag = Tag.parse_obj(tag_data)
+tag = await Resolver().resolve(tag)
+```
diff --git a/examples/15_tree.py b/examples/15_tree.py
new file mode 100644
index 0000000..6d900a0
--- /dev/null
+++ b/examples/15_tree.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+from pydantic import BaseModel
+from pydantic_resolve import Resolver
+from typing import List
+import asyncio
+
+class Tag(BaseModel):
+    name: str
+
+    full_path: str = ''
+    def resolve_full_path(self, parent):
+        if parent:
+            return f'{parent.full_path}/{self.name}'
+        else:
+            return self.name
+
+    children: List[Tag] = []
+
+
+tag_data = dict(name='root', children=[
+        dict(name='a', children=[
+            dict(name='b', children=[
+                dict(name='e', chidrent=[])
+            ])
+        ])
+    ])
+
+
+async def main():
+    tag = Tag.parse_obj(tag_data)
+    tag = await Resolver().resolve(tag)
+    print(tag.json(indent=4))
+    
+
+asyncio.run(main())
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index 208ac31..0bd4081 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -11,23 +11,42 @@ repo_name: allmonday/pydantic-resolve
 repo_url: https://github.com/allmonday/pydantic-resolve
 edit_uri: ""
 
+# nav:
+#   - Quick Start:
+#       - Install: ./v1/index.md
+#       - Let's start: ./v1/about.md
+#       - Dataloader: ./v1/dataloader.md
+#       - Inheritance: ./v1/inherit.md
+#       - Tree: ./v1/tree.md
+#       - Philosophy: ./v1/philosophy.md
+#   - Reference:
+#       - API: ./v1/reference_api.md
+#   - Change log:
+#       - pydantic-resolve: ./v1/changelog.md
+
 nav:
   - Quick Start:
-      - Install: index.md
-      - Let's start: about.md
-      - Dataloader: dataloader.md
-      - Inheritance: inherit.md
-      - Tree: tree.md
-      - Philosophy: philosophy.md
-  - Reference:
-      - API: reference_api.md
-  - Change log:
-      - pydantic-resolve: changelog.md
-      - pydantic2-resolve: changelog-v2.md
+      - Introduction: ./v2/introduction.md
+      - Use cases: ./v2/use_case.md
+      - Schema first: ./v2/schema_first.md
+  - Features:
+      - Dataloader: ./v2/dataloader.md
+      - Inherit and reuse: ./v2/inherit_reuse.md
+      - ERD Driven Pattern: ./v2/erd_driven.md
+      - Expose and collect: ./v2/expose_and_collect.md
+      - Connect to UI: ./v2/connect_to_ui.md
+  - API:
+      - Reference: ./v2/api.md
+      - Change log: ./v2/changelog.md
 
 markdown_extensions:
   - pymdownx.highlight:
       anchor_linenums: true
+  - pymdownx.superfences:
+      custom_fences:
+        - name: mermaid
+          class: mermaid
+          format: !!python/name:pymdownx.superfences.fence_code_format
   - pymdownx.superfences
 
 plugins:
@@ -45,7 +64,16 @@ plugins:
           link: "/pydantic-resolve/zh/"
           nav_translations:
             Quick Start: "快速开始"
+            Introduction: "简介"
+            Use cases: "应用场景"
+            Schema first: "先定义模型"
+            Features: "功能介绍"
+            Dataloader: "数据加载器"
+            Inherit and reuse: "继承和复用"
+            Expose and collect: "提供和收集"
             Reference: "文档"
+            Connect to UI: "连接UI"
+            ERD Driven Pattern: "ERD 驱动开发"
             Change log: "变更记录"
             Install: "安装"
             Let's start: "开始"
diff --git a/pydantic_resolve/__init__.py b/pydantic_resolve/__init__.py
index b27d8c2..9e0afda 100644
--- a/pydantic_resolve/__init__.py
+++ b/pydantic_resolve/__init__.py
@@ -1,19 +1,16 @@
+from .utils.collector import Collector, ICollector
+from .utils.class_util import ensure_subset
+from .utils.dataloader import build_list, build_object, copy_dataloader_kls
+from .utils.conversion import mapper
 from .exceptions import (
     ResolverTargetAttrNotFound,
-    DataloaderDependCantBeResolved,
     LoaderFieldNotProvidedError,
     MissingAnnotationError,
     GlobalLoaderFieldOverlappedError)
 from .resolver import Resolver
-from .core import LoaderDepend, Collector, ICollector
-from .util import (
-    build_list,
-    build_object,
-    mapper,
-    ensure_subset,
-    output,
-    model_config,
-    copy_dataloader_kls)
+from .utils.depend import LoaderDepend
+from .utils.openapi import (
+    model_config)
 
 
 __all__ = [
@@ -22,7 +19,6 @@
     'Collector',
     'ICollector',
     'ResolverTargetAttrNotFound',
-    'DataloaderDependCantBeResolved',
     'LoaderFieldNotProvidedError',
     'MissingAnnotationError',
     'GlobalLoaderFieldOverlappedError',
@@ -31,7 +27,6 @@
     'build_object',
     'mapper',
     'ensure_subset',
-    'output',
     'model_config',
     'copy_dataloader_kls',
 ]
\ No newline at end of file
diff --git a/pydantic_resolve/core.py b/pydantic_resolve/analysis.py
similarity index 61%
rename from pydantic_resolve/core.py
rename to pydantic_resolve/analysis.py
index c2edcb2..43204e0 100644
--- a/pydantic_resolve/core.py
+++ b/pydantic_resolve/analysis.py
@@ -1,91 +1,79 @@
-import abc
 import copy
 import inspect
+from typing import List, Type, Dict, Optional, Tuple
 from inspect import isfunction, isclass
 from collections import defaultdict
-from typing import Any, Callable, Optional
 from aiodataloader import DataLoader
 from pydantic import BaseModel
 from dataclasses import is_dataclass, fields as dc_fields
-import pydantic_resolve.util as util
+
 import pydantic_resolve.constant as const
-from .exceptions import ResolverTargetAttrNotFound, LoaderFieldNotProvidedError, MissingCollector
-from pydantic_resolve.compat import PYDANTIC_V2
-
-def LoaderDepend(  # noqa: N802
-    dependency: Optional[Callable[..., Any]] = None,
-) -> Any:
-    return Depends(dependency=dependency)
-
-class Depends:
-    def __init__(
-        self,
-        dependency: Optional[Callable[..., Any]] = None,
-    ):
-        self.dependency = dependency
-
-class ICollector(metaclass=abc.ABCMeta):
-    @abc.abstractmethod
-    def __init__(self, alias: str):
-        self.alias = alias  # required, must have
-
-    @abc.abstractmethod
-    def add(self, val):
-        """how to add new element(s)"""
-
-    @abc.abstractmethod
-    def values(self) -> Any:
-        """get result"""
-
-class Collector(ICollector):
-    def __init__(self, alias: str, flat: bool=False):
-        self.alias = alias
-        self.flat = flat
-        self.val = []
+import pydantic_resolve.utils.class_util as class_util
+from pydantic_resolve.utils.collector import ICollector
+from pydantic_resolve.utils.depend import Depends
+import pydantic_resolve.utils.params as params_util
+from pydantic_resolve.exceptions import ResolverTargetAttrNotFound, LoaderFieldNotProvidedError, MissingCollector
+import sys
+
+if sys.version_info >= (3, 8):
+    from typing import TypedDict
+else:
+    from typing_extensions import TypedDict
+
+class DataLoaderType(TypedDict):
+    param: str
+    kls: Type
+    path: str
+
+class CollectorType(TypedDict):
+    field: str 
+    param: str
+    instance: object
+    alias: str
     
-    def add(self, val):
-        if self.flat:
-            if isinstance(val, list):
-                self.val.extend(val)
-            else:
-                raise AttributeError('if flat, target should be list')
-        else:
-            self.val.append(val)
-
-    def values(self):
-        return self.val
-
-
-def _get_pydantic_attrs(kls):
-    if PYDANTIC_V2:
-        items = kls.model_fields.items()
-    else:
-        items = kls.__fields__.items()
-
-    for k, v in items:
-        t = v.annotation if PYDANTIC_V2 else v.type_
-
-        shelled_type = util.shelling_type(t)
-        if is_acceptable_kls(shelled_type):
-            yield (k, shelled_type)  # type_ is the most inner type
-
-
-def _get_dataclass_attrs(kls):
-    for name, v in kls.__annotations__.items():
-        shelled_type = util.shelling_type(v)
-        if is_acceptable_kls(shelled_type):
-            yield (name, shelled_type)
-
-
-def get_class(target):
-    if isinstance(target, list):
-        return target[0].__class__
-    else:
-        return target.__class__
-
-
-def _scan_resolve_method(method, field: str):
-    result = {
+class ResolveMethodType(TypedDict):
+    trim_field: str
+    context: bool
+    parent: bool
+    ancestor_context: bool
+    dataloaders: List[DataLoaderType]
+
+class PostMethodType(TypedDict):
+    trim_field: str
+    context: bool
+    parent: bool
+    ancestor_context: bool
+    collectors: List[CollectorType] 
+
+class PostDefaultHandlerType(TypedDict):
+    context: bool
+    parent: bool
+    ancestor_context: bool
+    collectors: List[CollectorType]
+
+class KlsMetaType(TypedDict):
+    resolve: List[str]
+    post: List[str]
+    resolve_params: Dict[str, ResolveMethodType]
+    post_params: Dict[str, PostMethodType]
+    post_default_handler_params: Optional[PostDefaultHandlerType]
+    object_fields: List[str]
+    expose_dict: dict
+    collect_dict: dict
+    kls: Type
+    has_context: bool
+
+MetaType = Dict[str, KlsMetaType]
+
+class MappedMetaMemberType(KlsMetaType):
+    kls_path: str
+    alias_map_proto: Dict[str, Dict[Tuple[str, str, str], object]]
+
+MappedMetaType = Dict[Type, MappedMetaMemberType]
+
+
+def _scan_resolve_method(method, field: str) -> ResolveMethodType:
+    result: ResolveMethodType = {
         'trim_field': field.replace(const.RESOLVE_PREFIX, ''),
         'context': False,
         'parent': False,
@@ -105,10 +93,10 @@ def _scan_resolve_method(method, field: str):
 
     for name, param in signature.parameters.items():
         if isinstance(param.default, Depends):
-            info = { 
+            info: DataLoaderType = { 
                 'param': name,
                 'kls': param.default.dependency,  # for later initialization
-                'path': util.get_kls_full_path(param.default.dependency) }
+                'path': class_util.get_kls_full_path(param.default.dependency) }
             result['dataloaders'].append(info)
 
     for name, param in signature.parameters.items():
@@ -118,8 +106,8 @@ def _scan_resolve_method(method, field: str):
     return result
 
 
-def _scan_post_method(method, field):
-    result = {
+def _scan_post_method(method, field) -> PostMethodType:
+    result: PostMethodType = {
         'trim_field': field.replace(const.POST_PREFIX, ''),
         'context': False,
         'ancestor_context': False,
@@ -139,7 +127,7 @@ def _scan_post_method(method, field):
     
     for name, param in signature.parameters.items():
         if isinstance(param.default, ICollector):
-            info = {
+            info: CollectorType = {
                 'field': field,
                 'param': name,
                 'instance': param.default,
@@ -150,8 +138,8 @@ def _scan_post_method(method, field):
     return result
 
 
-def _scan_post_default_handler(method):
-    result = {
+def _scan_post_default_handler(method) -> PostDefaultHandlerType:
+    result: PostDefaultHandlerType = {
         'context': False,
         'ancestor_context': False,
         'parent': False,
@@ -170,7 +158,7 @@ def _scan_post_default_handler(method):
 
     for name, param in signature.parameters.items():
         if isinstance(param.default, ICollector):
-            info = {
+            info: CollectorType = {
                 'field': const.POST_DEFAULT_HANDLER,
                 'param': name,
                 'instance': param.default,
@@ -212,11 +200,11 @@ def _create_instance(loader):
         loader_kls, path = loader['kls'], loader['path']
         if isclass(loader_kls):
             loader = loader_kls()
-            param_config = util.merge_dicts(
+            param_config = params_util.merge_dicts(
                 global_loader_param,
                 loader_params.get(loader_kls, {}))
 
-            for field in util.get_class_field_annotations(loader_kls):
+            for field in class_util.get_class_field_annotations(loader_kls):
                 try:
                     value = param_config[field]
                     setattr(loader, field, value)
@@ -241,30 +229,20 @@ def _create_instance(loader):
     return cache
 
 
-def scan_and_store_metadata(root_class):
-    """ see output data schema example in:
-    - test_field_dataclass_anno.py
-    - test_field_pydantic.py
+def scan_and_store_metadata(root_class: Type) -> MetaType:
 
-    rules: TODO:
-    - [x] dataloader and it's params
-    - [ ] context & ancestor_context are provided
-    - [ ] collector should be defined in ancestor of collect schemas.  """
-    util.update_forward_refs(root_class)
+    class_util.update_forward_refs(root_class)
     expose_set = set()  # for validation
     collect_set = set()  # for validation
-    metadata = {}
+    metadata: MetaType = {}
 
     def _get_all_fields_and_object_fields(kls):
-        if util.safe_issubclass(kls, BaseModel):
-            if PYDANTIC_V2:
-                all_fields = set(kls.model_fields.keys())
-            else:
-                all_fields = set(kls.__fields__.keys())
-            object_fields = list(_get_pydantic_attrs(kls))  # dive and recursively analysis
+        if class_util.safe_issubclass(kls, BaseModel):
+            all_fields = set(class_util.get_keys(kls))
+            object_fields = list(class_util.get_pydantic_attrs(kls))  # dive and recursively analysis
         elif is_dataclass(kls):
             all_fields = set([f.name for f in dc_fields(kls)])
-            object_fields = list(_get_dataclass_attrs(kls))
+            object_fields = list(class_util.get_dataclass_attrs(kls))
         else:
             raise AttributeError('invalid type: should be pydantic object or dataclass object')  #noqa
         return all_fields, object_fields
@@ -315,18 +293,30 @@ def _add_collector_info_for_default_handler(post_default_params):
                 collect_set.add(collector['alias'])
 
     def _validate_collector(collect_dict, kls_name):
-        """collector should be declared in ancestors"""
+        """
+        validate if the collector is declared in ancestor nodes.
+
+        available format of `__pydantic_resolve_collect__`
+
+        class A(BaseModel):
+            __pydantic_resolve_collect__ = {
+                'name': 'collector_a',                                  # send name to collector_a
+                ('name', 'age'): 'collector_b',                         # send ('name', 'age') to collector_b
+                'name': ('collector_c', 'collector_d'),                 # send name to both collector_c and d
+                ('name', 'age'): ('collector_e', 'collector_f'),        # send ('name', 'age') to both collector_e and f
+            }
+        """
         if type(collect_dict) is not dict:
-            raise AttributeError(f'{const.COLLECT_FROM_ANCESTOR} is not dict')
+            raise AttributeError(f'{const.COLLECTOR_CONFIGURATION} is not dict')
 
         for _, collector in collect_dict.items():
-            colls = collector if isinstance(collector, (list, tuple)) else (collector,)
-            for col in colls:
-                if col not in collect_set:
+            collectors = collector if isinstance(collector, (list, tuple)) else (collector,)
+            for c in collectors:
+                if c not in collect_set:
                     raise MissingCollector(f'Collector alias name not found in ancestor, please check: {kls_name}')
 
     def walker(kls):
-        kls_name = util.get_kls_full_path(kls)
+        kls_name = class_util.get_kls_full_path(kls)
         hit = metadata.get(kls_name)
         if hit: return
 
@@ -338,7 +328,7 @@ def walker(kls):
 
         # - scan expose and collect (__pydantic_resolve_xxx__)
         expose_dict = getattr(kls, const.EXPOSE_TO_DESCENDANT, {})
-        collect_dict = getattr(kls, const.COLLECT_FROM_ANCESTOR, {})
+        collect_dict = getattr(kls, const.COLLECTOR_CONFIGURATION, {})
         _validate_expose(expose_dict, kls_name)
 
         resolve_params = {field: _scan_resolve_method(getattr(kls, field), field) for field in resolve_fields}
@@ -356,18 +346,19 @@ def walker(kls):
         _add_collector_info_for_default_handler(post_default_handler_params)
         _validate_collector(collect_dict, kls_name)
 
-        metadata[kls_name] = {
+        info: KlsMetaType = {
             'resolve': resolve_fields,
             'resolve_params': resolve_params,
             'post': post_fields,
             'post_params': post_params,
             'post_default_handler_params': post_default_handler_params,
-            'attribute': object_fields_without_resolver,
+            'object_fields': object_fields_without_resolver,
             'expose_dict': expose_dict,
             'collect_dict': collect_dict,
             'kls': kls,
             'has_context': has_context,
         }
+        metadata[kls_name] = info
 
         for _, shelled_type in object_fields:
             walker(shelled_type)
@@ -376,92 +367,127 @@ def walker(kls):
     return metadata
 
 
-def convert_metadata_key_as_kls(metadata):
+def convert_metadata_key_as_kls(metadata: MetaType) -> MappedMetaType:
     """ use kls as map key for performance """
     kls_metadata = {}
     for k, v in metadata.items():
-        v['kls_path'] = k
-        kls_metadata[v['kls']] = v
+        _kls_meta: MappedMetaMemberType = {
+            **v, 
+            'kls_path':k,
+            'alias_map_proto': {}
+        }
+        alias_map = _calc_alias_map_from_collectors(_kls_meta)
+        _kls_meta['alias_map_proto'] = alias_map
+        kls_metadata[v['kls']] = _kls_meta
     
     return kls_metadata
 
 
-def is_acceptable_kls(kls):
-    return util.safe_issubclass(kls, BaseModel) or is_dataclass(kls)
+def _calc_alias_map_from_collectors(kls_meta: MappedMetaMemberType):
+    post_params = kls_meta['post_params']
+    kls_path = kls_meta['kls_path']
+
+    alias_map = defaultdict(dict)
 
+    def add_collector(collector: CollectorType):
+        """
+        class A(BaseModel):
+            def post_name(collector=Collector('name')):
+                ...
+        kls_path: module_name.A
+        field: post_name
+        param: collector_a
+
+        sign = ('module_name.A', 'post_name', 'collector_a')
+
+        return { 'name': { sign: collector_instance } }
+        """
+        sign = get_collector_sign(kls_path, collector)
+        alias_map[collector['alias']][sign] = collector['instance']
+
+    for _, param in post_params.items():
+        for collector in param['collectors']:
+            add_collector(collector)
 
-def is_acceptable_instance(target):
+    # post_default_handler
+    post_default_handler_params = kls_meta['post_default_handler_params']
+    if post_default_handler_params:
+        for collector in post_default_handler_params['collectors']:
+            add_collector(collector)
+
+    return alias_map
+
+
+def is_acceptable_kls(kls: Type) -> bool:
+    return class_util.safe_issubclass(kls, BaseModel) or is_dataclass(kls)
+
+
+def is_acceptable_instance(target: object):
     """ check whether target is Pydantic object or Dataclass object """
     return isinstance(target, BaseModel) or is_dataclass(target)
 
 
-def iter_over_object_resolvers_and_acceptable_fields(target, kls, metadata):
-    """metadata key is kls"""
-    kls_meta = metadata.get(kls)
+def get_resolve_fields_and_object_fields_from_object(node: object, kls: Type, mapped_metadata: MappedMetaType):
+    """
+    resolve_fields: a field with resolve_field method
+    object_fields: a field without resolve_field method but is an object (should traversal)
+    """
+    resolve_fields, object_fields = [], []
+    kls_meta = mapped_metadata.get(kls)
 
-    resolve, attribute = [], []
+    if kls_meta is None:
+        return [], []
 
-    for resolve_field in kls_meta['resolve']:  # eg: resolve_name
-        attr = getattr(target, resolve_field)
+    for resolve_field in kls_meta['resolve']:
+        attr = getattr(node, resolve_field)
         trim_field = kls_meta['resolve_params'][resolve_field]['trim_field']
-        resolve.append((resolve_field, trim_field, attr))
+        resolve_fields.append((resolve_field, trim_field, attr))
 
-    for attr_name in kls_meta['attribute']:
-        attr = getattr(target, attr_name)
-        attribute.append((attr_name, attr))
+    for attr_name in kls_meta['object_fields']:
+        attr = getattr(node, attr_name)
+        object_fields.append((attr_name, attr))
 
-    return resolve, attribute
+    return resolve_fields, object_fields
 
 
-def iter_over_object_post_methods(kls, metadata):
-    """metadata key is kls"""
-    kls_meta = metadata.get(kls)
+def get_post_methods(kls: Type, mapped_metadata: MappedMetaType):
+    kls_meta = mapped_metadata.get(kls)
+
+    if kls_meta is None:
+        return []
 
     for post_field in kls_meta['post']:
         trim_field = kls_meta['post_params'][post_field]['trim_field']
         yield post_field, trim_field
 
 
-def get_resolve_param(kls, resolve_field, metadata):
-    kls_meta = metadata.get(kls, {})
+def get_resolve_method_param(kls: Type, resolve_field: str, mapped_metadata: MappedMetaType):
+    kls_meta = mapped_metadata.get(kls, {})
     return kls_meta['resolve_params'][resolve_field]
 
 
-def get_post_params(kls, post_field, metadata):
-    kls_meta = metadata.get(kls, {})
+def get_post_method_params(kls, post_field, mapped_metadata: MappedMetaType):
+    kls_meta = mapped_metadata.get(kls, {})
     return kls_meta['post_params'][post_field]
 
 
-def get_post_default_handler_params(kls, metadata):
-    kls_meta = metadata.get(kls, {})
+def get_post_default_handler_params(kls, mapped_metadata: MappedMetaType):
+    kls_meta = mapped_metadata.get(kls, {})
     return kls_meta['post_default_handler_params']
 
 
-def get_collectors(kls, metadata):
-    kls_meta = metadata.get(kls, {})
-
-    # post method
-    post_params = kls_meta['post_params']
-    kls_path = kls_meta['kls_path']
-
-    alias_map = defaultdict(dict)
-    for _, param in post_params.items():
-        for collector in param['collectors']:
-            sign = (kls_path, collector['field'], collector['param'])
-            # copied instance will be stored in resolver's self.object_collect_alias_map_store
-            alias_map[collector['alias']][sign] = copy.deepcopy(collector['instance'])
+def get_collector_sign(kls_path: str, collector: CollectorType):
+    return (kls_path, collector['field'], collector['param'])
 
-    # post_default_handler
-    post_default_handler_params = kls_meta['post_default_handler_params']
-    if post_default_handler_params:
-        for collector in post_default_handler_params['collectors']:
-            sign = (kls_path, collector['field'], collector['param'])
-            alias_map[collector['alias']][sign] = copy.deepcopy(collector['instance'])
 
-    return alias_map
+def generate_alias_map_with_cloned_collector(kls: Type, mapped_metadata: MappedMetaType):
+    kls_meta = mapped_metadata.get(kls, {})
+    return { alias: {
+        sign: copy.deepcopy(collector) for sign, collector in v.items()
+    } for alias, v in kls_meta['alias_map_proto'].items()}
 
 
-def iter_over_collectable_fields(kls, metadata):
+def get_collector_candidates(kls, metadata: MappedMetaType):
     kls_meta = metadata.get(kls, {})
     collect_dict = kls_meta['collect_dict']
 
@@ -469,5 +495,5 @@ def iter_over_collectable_fields(kls, metadata):
         yield field, alias
 
 
-def has_context(metadata):
-    return any([ m['has_context'] for m in metadata.values()])
\ No newline at end of file
+def has_context(mapped_metadata: MappedMetaType):
+    return any([ m['has_context'] for m in mapped_metadata.values()])
\ No newline at end of file
diff --git a/pydantic_resolve/constant.py b/pydantic_resolve/constant.py
index ae06d4a..cc173f0 100644
--- a/pydantic_resolve/constant.py
+++ b/pydantic_resolve/constant.py
@@ -5,4 +5,4 @@
 HAS_MAPPER_FUNCTION = '__pydantic_resolve_mapper_provided__'
 POST_DEFAULT_HANDLER = 'post_default_handler'
 EXPOSE_TO_DESCENDANT = '__pydantic_resolve_expose__'
-COLLECT_FROM_ANCESTOR = '__pydantic_resolve_collect__'
+COLLECTOR_CONFIGURATION = '__pydantic_resolve_collect__'
diff --git a/pydantic_resolve/exceptions.py b/pydantic_resolve/exceptions.py
index 1066fc2..6411d19 100644
--- a/pydantic_resolve/exceptions.py
+++ b/pydantic_resolve/exceptions.py
@@ -1,9 +1,6 @@
 class ResolverTargetAttrNotFound(Exception):
     pass
 
-class DataloaderDependCantBeResolved(Exception):
-    pass
-
 class LoaderFieldNotProvidedError(Exception):
     pass
 
diff --git a/pydantic_resolve/resolver.py b/pydantic_resolve/resolver.py
index 976728b..12fbaf2 100644
--- a/pydantic_resolve/resolver.py
+++ b/pydantic_resolve/resolver.py
@@ -2,14 +2,15 @@
 import contextvars
 import warnings
 from inspect import iscoroutine
-from typing import TypeVar, Dict
-from .exceptions import MissingAnnotationError
-from typing import Any, Optional
-from pydantic_resolve import core
+from typing import TypeVar, Dict, Type, Callable, Any, Optional
 from aiodataloader import DataLoader
 from types import MappingProxyType
+
+from pydantic_resolve import analysis
+from pydantic_resolve.exceptions import MissingAnnotationError
+import pydantic_resolve.utils.conversion as conversion_util
+import pydantic_resolve.utils.class_util as class_util
 import pydantic_resolve.constant as const
-import pydantic_resolve.util as util
 
 
 T = TypeVar("T")
@@ -54,7 +55,7 @@ def __init__(
         self.ensure_type = ensure_type
         self.context = MappingProxyType(context) if context else None
         self.metadata = {}
-        self.object_collect_alias_map_store = {}
+        self.object_level_collect_alias_map_store: Dict[int, Dict] = {}
 
     def _validate_loader_instance(self, loader_instances: Dict[Any, Any]):
         for cls, loader in loader_instances.items():
@@ -64,46 +65,36 @@ def _validate_loader_instance(self, loader_instances: Dict[Any, Any]):
                 raise AttributeError(f'{loader.__name__} is not instance of {cls.__name__}')
         return True
     
-    def _prepare_collectors(self, target, kls):
-        alias_map = core.get_collectors(kls, self.metadata)
+    def _prepare_collectors(self, node: object, kls: Type):
+        alias_map = analysis.generate_alias_map_with_cloned_collector(kls, self.metadata)
         if alias_map:
-            self.object_collect_alias_map_store[id(target)] = alias_map
+            # store for later post methods
+            self.object_level_collect_alias_map_store[id(node)] = alias_map  
 
-            for alias, sign_collector_pair in alias_map.items():
-                if not self.collector_contextvars.get(alias):
-                    self.collector_contextvars[alias] = contextvars.ContextVar(alias, default={})
+            # expose to descendant
+            for alias_name, sign_collector_kv in alias_map.items():
+                if not self.collector_contextvars.get(alias_name):
+                    self.collector_contextvars[alias_name] = contextvars.ContextVar(alias_name, default={})
                 
-                current_pair = self.collector_contextvars[alias].get()
-                updated_pair = {**current_pair, **sign_collector_pair}
-                self.collector_contextvars[alias].set(updated_pair)
-
-    def _add_values_into_collectors(self, target, kls):
-        for field, alias in core.iter_over_collectable_fields(kls, self.metadata):
-            # handle two scenarios
-            # {'name': ('collector_a', 'collector_b')}
-            # {'name': 'collector_a'}
-            alias_list = alias if isinstance(alias, (tuple, list)) else (alias,)
+                current_pair = self.collector_contextvars[alias_name].get()
+                if set(sign_collector_kv.keys()) - set(current_pair.keys()):  # update only when new sign is found
+                    updated_pair = {**current_pair, **sign_collector_kv}
+                    self.collector_contextvars[alias_name].set(updated_pair)
 
-            for alias in alias_list:
-                for _, instance in self.collector_contextvars[alias].get().items():
-                    val = [getattr(target, f) for f in field]\
-                        if isinstance(field, tuple) else getattr(target, field)
-                    instance.add(val)
-    
-    def _add_parent(self, target):
+    def _prepare_parent(self, node: object):
         if not self.parent_contextvars.get('parent'):
             self.parent_contextvars['parent'] = contextvars.ContextVar('parent')
-        self.parent_contextvars['parent'].set(target)
+        self.parent_contextvars['parent'].set(node)
 
-    def _add_expose_fields(self, target):
-        expose_dict: Optional[dict] = getattr(target, const.EXPOSE_TO_DESCENDANT, None)
+    def _prepare_expose_fields(self, node: object):
+        expose_dict: Optional[dict] = getattr(node, const.EXPOSE_TO_DESCENDANT, None)
         if expose_dict:
             for field, alias in expose_dict.items():  # eg: {'name': 'bar_name'}
                 if not self.ancestor_vars.get(alias):
                     self.ancestor_vars[alias] = contextvars.ContextVar(alias)
 
                 try:
-                    val = getattr(target, field)
+                    val = getattr(node, field)
                 except AttributeError:
                     raise AttributeError(f'{field} does not existed')
 
@@ -112,9 +103,15 @@ def _add_expose_fields(self, target):
     def _prepare_ancestor_context(self):
         return {k: v.get() for k, v in self.ancestor_vars.items()}
 
-    def _execute_resolver_method(self, kls, field, method):
+    def _execute_resolve_method(
+            self,
+            kls: Type,
+            field: str,
+            method: Callable):
+
         params = {}
-        resolve_param = core.get_resolve_param(kls, field, self.metadata)
+        resolve_param = analysis.get_resolve_method_param(kls, field, self.metadata)
+
         if resolve_param['context']:
             params['context'] = self.context
         if resolve_param['ancestor_context']:
@@ -129,9 +126,17 @@ def _execute_resolver_method(self, kls, field, method):
 
         return method(**params)
     
-    def _execute_post_method(self, target, kls, kls_path, post_field, method):
+    def _execute_post_method(
+            self,
+            node: object,
+            kls: Type,
+            kls_path: str,
+            post_field: str,
+            method: Callable):
+
         params = {}
-        post_param = core.get_post_params(kls, post_field , self.metadata)
+        post_param = analysis.get_post_method_params(kls, post_field , self.metadata)
+
         if post_param['context']:
             params['context'] = self.context
         if post_param['ancestor_context']:
@@ -139,18 +144,27 @@ def _execute_post_method(self, target, kls, kls_path, post_field, method):
         if post_param['parent']:
             params['parent'] = self.parent_contextvars['parent'].get()
 
-        alias_map = self.object_collect_alias_map_store.get(id(target), {})
+        alias_map = self.object_level_collect_alias_map_store.get(id(node), {})
         if alias_map:
             for collector in post_param['collectors']:
+                signature = analysis.get_collector_sign(kls_path, collector)
                 alias, param = collector['alias'], collector['param']
-                signature = (kls_path, post_field, param)
                 params[param] = alias_map[alias][signature]
         
         return method(**params)
 
-    def _execute_post_default_handler(self, target, kls, kls_path, method):
+    def _execute_post_default_handler(
+            self,
+            node: object,
+            kls: Type,
+            kls_path: str,
+            method: Callable):
+
         params = {}
-        post_default_param = core.get_post_default_handler_params(kls, self.metadata)
+        post_default_param = analysis.get_post_default_handler_params(kls, self.metadata)
+
+        if post_default_param is None:
+            return
 
         if post_default_param['context']:
             params['context'] = self.context
@@ -159,7 +173,7 @@ def _execute_post_default_handler(self, target, kls, kls_path, method):
         if post_default_param['parent']:
             params['parent'] = self.parent_contextvars['parent'].get()
 
-        alias_map = self.object_collect_alias_map_store.get(id(target), {})
+        alias_map = self.object_level_collect_alias_map_store.get(id(node), {})
         if alias_map:
             for collector in post_default_param['collectors']:
                 alias, param = collector['alias'], collector['param']
@@ -168,83 +182,119 @@ def _execute_post_default_handler(self, target, kls, kls_path, method):
 
         return method(**params)
 
-    async def _resolve_obj_field(self, target, kls, field, trim_field, method):
+    def _add_values_into_collectors(self, node: object, kls: Type):
+        for field, alias in analysis.get_collector_candidates(kls, self.metadata):
+            alias_list = alias if isinstance(alias, (tuple, list)) else (alias,)
+
+            for alias in alias_list:
+                for _, instance in self.collector_contextvars[alias].get().items():
+                    if isinstance(field, tuple):  # only tuple are allowed to be key
+                        val = [getattr(node, f) for f in field]
+                    else:
+                        val = getattr(node, field)
+                    instance.add(val)
+
+    async def _execute_resolve_method_field(
+            self, 
+            node: object, 
+            kls: Type,
+            field: str,
+            trim_field: str,
+            method: Callable):
         if self.ensure_type:
             if not method.__annotations__:
                 raise MissingAnnotationError(f'{field}: return annotation is required')
 
-        val = self._execute_resolver_method(kls, field, method)
+        val = self._execute_resolve_method(kls, field, method)
         while iscoroutine(val) or asyncio.isfuture(val):
             val = await val
 
         if not getattr(method, const.HAS_MAPPER_FUNCTION, False):  # defined in util.mapper
-            val = util.try_parse_data_to_target_field_type(target, trim_field, val)
+            val = conversion_util.try_parse_data_to_target_field_type(node, trim_field, val)
 
         # continue dive deeper
-        val = await self._resolve(val, target)
-
-        setattr(target, trim_field, val)
-
-    async def _resolve(self, target: T, parent) -> T:
-        if isinstance(target, (list, tuple)):
-            # list should not play as parent, use original parent.
-            await asyncio.gather(*[self._resolve(t, parent) for t in target])
-
-        if core.is_acceptable_instance(target):
-            kls = target.__class__
-            kls_path = util.get_kls_full_path(kls)
-
-            self._prepare_collectors(target, kls)
-            self._add_expose_fields(target)
-            self._add_parent(parent)
-
-            tasks = []
-
-            # traversal and fetching data by resolve methods
-            resolve_list, attribute_list = core.iter_over_object_resolvers_and_acceptable_fields(target, kls, self.metadata)
-            for field, resolve_trim_field, method in resolve_list:
-                tasks.append(self._resolve_obj_field(target, kls, field, resolve_trim_field, method))
-            for field, attr_object in attribute_list:
-                tasks.append(self._resolve(attr_object, target))
-            await asyncio.gather(*tasks)
-
-            # reverse traversal and run post methods
-            for post_field, post_trim_field in core.iter_over_object_post_methods(kls, self.metadata):
-                post_method = getattr(target, post_field)
-                result = self._execute_post_method(target, kls, kls_path, post_field, post_method)
-
-                # TODO:  post method support async, should be gathered instead of for + await
-                while iscoroutine(result) or asyncio.isfuture(result):
-                    result = await result
-                    
-                result = util.try_parse_data_to_target_field_type(target, post_trim_field, result)
-                setattr(target, post_trim_field, result)
-
-            default_post_method = getattr(target, const.POST_DEFAULT_HANDLER, None)
-            if default_post_method:
-                self._execute_post_default_handler(target, kls, kls_path, default_post_method)
+        val = await self._traverse(val, node)
+
+        setattr(node, trim_field, val)
+
+    async def _traverse(self, node: T, parent: object) -> T:
+        """
+        life cycle:
+        - prepare 
+            - collectors, 
+            - ancestor expose fields 
+            - parent
+        - execute 
+            - resolve method fields/ object fields
+            - post method fields
+            - post default handler
+        - collect
+            - values into ancestor collectors
+        - return node
+
+        TODO:
+        If the descendant of object has no more pydantic-resolve related operation, it should be skipped.
+        """
+        if isinstance(node, (list, tuple)):
+            await asyncio.gather(*[self._traverse(t, parent) for t in node])
+
+        if not analysis.is_acceptable_instance(node):
+            return node
+
+        kls = node.__class__
+        kls_path = class_util.get_kls_full_path(kls)
+
+        self._prepare_collectors(node, kls)
+        self._prepare_expose_fields(node)
+        self._prepare_parent(parent)
+
+        tasks = []
+
+        # resolve process
+        resolve_fields, object_fields = analysis.get_resolve_fields_and_object_fields_from_object(node, kls, self.metadata)
+        for field, resolve_trim_field, method in resolve_fields:
+            tasks.append(self._execute_resolve_method_field(node, kls, field, resolve_trim_field, method))
+
+        for field, attr_object in object_fields:
+            tasks.append(self._traverse(attr_object, node))
+
+        await asyncio.gather(*tasks)
+
+        # post process
+        for post_field, post_trim_field in analysis.get_post_methods(kls, self.metadata):
+            post_method = getattr(node, post_field)
+            result = self._execute_post_method(node, kls, kls_path, post_field, post_method)
+
+            # although post method support async, but not recommended to use 
+            while iscoroutine(result) or asyncio.isfuture(result):
+                result = await result
+                
+            result = conversion_util.try_parse_data_to_target_field_type(node, post_trim_field, result)
+            setattr(node, post_trim_field, result)
 
-            # collect after all done
-            self._add_values_into_collectors(target, kls)
+        default_post_method = getattr(node, const.POST_DEFAULT_HANDLER, None)
+        if default_post_method:
+            self._execute_post_default_handler(node, kls, kls_path, default_post_method)
 
-        return target
+        self._add_values_into_collectors(node, kls)
+        return node
 
-    async def resolve(self, target: T) -> T:
-        if isinstance(target, list) and target == []: return target
+    async def resolve(self, node: T) -> T:
+        if isinstance(node, list) and node == []: return node
 
-        root_class = core.get_class(target)
-        metadata = core.scan_and_store_metadata(root_class)
-        self.metadata = core.convert_metadata_key_as_kls(metadata)
+        root_class = class_util.get_class(node)
+        metadata = analysis.scan_and_store_metadata(root_class)
+        self.metadata = analysis.convert_metadata_key_as_kls(metadata)
 
-        self.loader_instance_cache = core.validate_and_create_loader_instance(
+        self.loader_instance_cache = analysis.validate_and_create_loader_instance(
             self.loader_params,
             self.global_loader_param,
             self.loader_instances,
             self.metadata)
         
-        has_context = core.has_context(self.metadata)
+        has_context = analysis.has_context(self.metadata)
         if has_context and self.context is None:
             raise AttributeError('context is missing')
             
-        await self._resolve(target, None)
-        return target
\ No newline at end of file
+        await self._traverse(node, None)
+        return node
\ No newline at end of file
diff --git a/pydantic_resolve/util.py b/pydantic_resolve/util.py
deleted file mode 100644
index 23b899b..0000000
--- a/pydantic_resolve/util.py
+++ /dev/null
@@ -1,547 +0,0 @@
-import asyncio
-import types
-import functools
-from collections import defaultdict
-from dataclasses import is_dataclass
-from inspect import iscoroutine, isfunction
-from typing import Any, DefaultDict, Sequence, Type, TypeVar, List, Callable, Optional, Mapping, Union, Iterator, Dict, get_type_hints
-import pydantic_resolve.constant as const
-from pydantic_resolve.exceptions import GlobalLoaderFieldOverlappedError
-from aiodataloader import DataLoader
-from pydantic_resolve.compat import PYDANTIC_V2
-
-if PYDANTIC_V2:
-    from pydantic import BaseModel, TypeAdapter, ValidationError
-else:
-    from pydantic import BaseModel, parse_obj_as, ValidationError
-
-def get_class_field_annotations(cls: Type):
-    anno = cls.__dict__.get('__annotations__') or {}
-    return anno.keys()
-
-
-T = TypeVar("T")
-V = TypeVar("V")
-
-if PYDANTIC_V2:
-    class TypeAdapterManager:
-        apapters = {}
-        
-        @classmethod
-        def get(cls, type):
-            adapter = cls.apapters.get(type)
-            if adapter:
-                return adapter
-            else:
-                new_adapter = TypeAdapter(type)
-                cls.apapters[type] = new_adapter
-                return new_adapter
-
-def safe_issubclass(kls, classinfo):
-    try:
-        return issubclass(kls, classinfo)
-    except TypeError:
-        return False
-
-def merge_dicts(a: Dict[str, Any], b: Dict[str, Any]):
-    overlap = set(a.keys()) & set(b.keys())
-    if overlap:
-        raise GlobalLoaderFieldOverlappedError(f'loader_params and global_loader_param have duplicated key(s): {",".join(overlap)}')
-    else:
-        return {**a, **b}
-
-def build_object(items: Sequence[T], keys: List[V], get_pk: Callable[[T], V]) -> Iterator[Optional[T]]:
-    """
-    helper function to build return object data required by aiodataloader
-    """
-    dct: Mapping[V, T] = {}
-    for item in items:
-        _key = get_pk(item)
-        dct[_key] = item
-    results = (dct.get(k, None) for k in keys)
-    return results
-
-def build_list(items: Sequence[T], keys: List[V], get_pk: Callable[[T], V]) -> Iterator[List[T]]:
-    """
-    helper function to build return list data required by aiodataloader
-    """
-    dct: DefaultDict[V, List[T]] = defaultdict(list) 
-    for item in items:
-        _key = get_pk(item)
-        dct[_key].append(item)
-    results = (dct.get(k, []) for k in keys)
-    return results
-
-def replace_method(cls: Type, cls_name: str, func_name: str, func: Callable):
-    """test-only"""
-    KLS = type(cls_name, (cls,), {func_name: func})
-    return KLS
-
-def get_required_fields(kls: BaseModel):
-    required_fields = []
-
-    def _is_require(field):
-        if PYDANTIC_V2:
-            return field.is_required()
-        else:
-            return field.required
-
-    # 1. get required fields
-    if PYDANTIC_V2:
-        items = kls.model_fields.items()
-    else:
-        items = kls.__fields__.items()
-    
-
-    for fname, field in items:
-        if _is_require(field):
-            required_fields.append(fname)
-        
-
-    # 2. get resolve_ and post_ target fields
-    for f in dir(kls):
-        if f.startswith(const.RESOLVE_PREFIX):
-            if isfunction(getattr(kls, f)):
-                required_fields.append(f.replace(const.RESOLVE_PREFIX, ''))
-
-        if f.startswith(const.POST_PREFIX):
-            if isfunction(getattr(kls, f)):
-                required_fields.append(f.replace(const.POST_PREFIX, ''))
-
-    return required_fields
-
-
-def output_v1(kls):
-    """
-    set required as True for all fields, make typescript code gen result friendly to use
-    """
-    if safe_issubclass(kls, BaseModel):
-        def _schema_extra(schema: Dict[str, Any], model) -> None:
-            fnames = get_required_fields(model)
-            schema['required'] = fnames
-
-        kls.__config__.schema_extra = staticmethod(_schema_extra)
-
-    else:
-        raise AttributeError(f'target class {kls.__name__} is not BaseModel')
-    return kls
-
-def output_v2(kls):
-    """
-    set required as True for all fields
-    make typescript code gen result friendly to use
-    """
-
-    if safe_issubclass(kls, BaseModel):
-
-        def build():
-            def schema_extra(schema: Dict[str, Any], model) -> None:
-                fnames = get_required_fields(model)
-                schema['required'] = fnames
-            return schema_extra
-
-        kls.model_config['json_schema_extra'] = staticmethod(build())
-
-    else:
-        raise AttributeError(f'target class {kls.__name__} is not BaseModel')
-    return kls
-
-output = output_v2 if PYDANTIC_V2 else output_v1
-
-
-def model_config_v1(default_required: bool = True):
-    """
-    - hidden_fields: fields want to hide
-    - default_required: 
-        if resolve field has default value, it will not be listed in schema['required']
-        set default_required=True to add it into required list.
-    """
-    def wrapper(kls):
-        if safe_issubclass(kls, BaseModel):
-                
-            # override schema_extra method
-            def _schema_extra(schema: Dict[str, Any], model) -> None:
-                # define schema.properties
-                excludes = set()
-
-                if kls.__exclude_fields__:
-                    for k in kls.__exclude_fields__.keys():
-                        excludes.add(k)
-
-                props = {}
-                for k, v in schema.get('properties', {}).items():
-                    if k not in excludes:
-                        props[k] = v
-                schema['properties'] = props
-
-                # define schema.required
-                if default_required:
-                    fnames = get_required_fields(model)
-                    schema['required'] = fnames
-            kls.__config__.schema_extra = staticmethod(_schema_extra)
-        else:
-            raise AttributeError(f'target class {kls.__name__} is not BaseModel')
-        return kls
-    return wrapper
-
-def model_config_v2(default_required: bool=True):
-    """
-    in pydantic v2, we can not use __exclude_field__ to set hidden field in model_config hidden_field params
-    model_config now is just a simple decorator to remove fields (with exclude=True) from schema.properties
-    and set schema.required for better schema description. 
-    (same like `output` decorator, you can replace output with model_config)
-
-    it keeps the form of model_config(params) in order to extend new features in future
-    """
-    def wrapper(kls):
-        if safe_issubclass(kls, BaseModel):
-            def build():
-                def _schema_extra(schema: Dict[str, Any], model) -> None:
-                    # 1. collect exclude fields and then hide in both schema and dump (default action)
-                    excluded_fields = [k for k, v in kls.model_fields.items() if v.exclude == True]
-                    props = {}
-
-                    # config schema properties
-                    for k, v in schema.get('properties', {}).items():
-                        if k not in excluded_fields:
-                            props[k] = v
-                    schema['properties'] = props
-
-                    # config schema required (fields with default values will not be listed in required field)
-                    # and the generated typescript models will define it as optional, and is troublesome in use
-                    if default_required:
-                        fnames = get_required_fields(model)
-                        if excluded_fields:
-                            fnames = [n for n in fnames if n not in excluded_fields]
-                        schema['required'] = fnames
-
-                return _schema_extra
-
-            kls.model_config['json_schema_extra'] = staticmethod(build())
-        else:
-            raise AttributeError(f'target class {kls.__name__} is not BaseModel')
-        return kls
-    return wrapper
-
-model_config = model_config_v2 if PYDANTIC_V2 else model_config_v1
-
-
-def mapper(func_or_class: Union[Callable, Type]):
-    """
-    execute post-transform function after the value is reolved
-    func_or_class:
-        is func: run func
-        is class: call auto_mapping to have a try
-    """
-    def inner(inner_fn):
-
-        # if mapper provided, auto map from target type will be disabled
-        setattr(inner_fn, const.HAS_MAPPER_FUNCTION, True)
-
-        @functools.wraps(inner_fn)
-        async def wrap(*args, **kwargs):
-
-            retVal = inner_fn(*args, **kwargs)
-            while iscoroutine(retVal) or asyncio.isfuture(retVal):
-                retVal = await retVal  # get final result
-            
-            if retVal is None:
-                return None
-
-            if isinstance(func_or_class, types.FunctionType):
-                # manual mapping
-                return func_or_class(retVal)
-            else:
-                # auto mapping
-                if isinstance(retVal, list):
-                    if retVal:
-                        rule = _get_mapping_rule(func_or_class, retVal[0])
-                        return _apply_rule(rule, func_or_class, retVal, True)
-                    else:
-                        return retVal  # return []
-                else:
-                    rule = _get_mapping_rule(func_or_class, retVal)
-                    return _apply_rule(rule, func_or_class, retVal, False)
-        return wrap
-    return inner
-
-def _get_mapping_rule_v1(target, source) -> Optional[Callable]:
-    # do noting
-    if isinstance(source, target):
-        return None
-
-    # pydantic
-    if safe_issubclass(target, BaseModel):
-        if target.__config__.orm_mode:
-            if isinstance(source, dict):
-                raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic from_orm can't handle dict object")
-            else:
-                return lambda t, s: t.from_orm(s)
-
-        if isinstance(source, (dict, BaseModel)):
-            return lambda t, s: t.parse_obj(s)
-
-        else:
-            raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic can't handle non-dict data")
-
-    # dataclass
-    if is_dataclass(target):
-        if isinstance(source, dict):
-            return lambda t, s: t(**s)
-
-    raise NotImplementedError(f"{type(source)} -> {target.__name__}: faild to get auto mapping rule and execut mapping, use your own rule instead.")
-
-def _get_mapping_rule_v2(target, source) -> Optional[Callable]:
-    # do noting
-    if isinstance(source, target):
-        return None
-
-    # pydantic
-    if safe_issubclass(target, BaseModel):
-        if target.model_config.get('from_attributes'):
-            if isinstance(source, dict):
-                raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic from_orm can't handle dict object")
-            else:
-                return lambda t, s: t.model_validate(s)
-
-        if isinstance(source, dict):
-            return lambda t, s: t.model_validate(s)
-
-        if isinstance(source, BaseModel):
-            if source.model_config.get('from_attributes'):
-                return lambda t, s: t.model_validate(s) 
-            else:
-                return lambda t, s: t(**s.model_dump()) 
-
-        else:
-            raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic can't handle non-dict data")
-    
-    # dataclass
-    if is_dataclass(target):
-        if isinstance(source, dict):
-            return lambda t, s: t(**s)
-    
-    raise NotImplementedError(f"{type(source)} -> {target.__name__}: faild to get auto mapping rule and execut mapping, use your own rule instead.")
-
-_get_mapping_rule = _get_mapping_rule_v2 if PYDANTIC_V2 else _get_mapping_rule_v1
-
-
-def _apply_rule(rule: Optional[Callable], target, source: Any, is_list: bool):
-    if not rule:  # no change
-        return source
-
-    if is_list:
-        return [rule(target, s) for s in source]
-    else:
-        return rule(target, source)
-
-def ensure_subset_v1(base):
-    """
-    used with pydantic class to make sure a class's field is 
-    subset of target class
-    """
-    def wrap(kls):
-        assert safe_issubclass(base, BaseModel), 'base should be pydantic class'
-        assert safe_issubclass(kls, BaseModel), 'class should be pydantic class'
-
-        @functools.wraps(kls)
-        def inner():
-            for k, field in kls.__fields__.items():
-                if field.required:
-                    base_field = base.__fields__.get(k)
-                    if not base_field:
-                        raise AttributeError(f'{k} not existed in {base.__name__}.')
-                    if base_field and base_field.type_ != field.type_:
-                        raise AttributeError(f'type of {k} not consistent with {base.__name__}'  )
-            return kls
-        return inner()
-    return wrap 
-
-def ensure_subset_v2(base):
-    """
-    used with pydantic class to make sure a class's field is 
-    subset of target class
-    """
-    def wrap(kls):
-        assert safe_issubclass(base, BaseModel), 'base should be pydantic class'
-        assert safe_issubclass(kls, BaseModel), 'class should be pydantic class'
-
-        @functools.wraps(kls)
-        def inner():
-            for k, field in kls.model_fields.items():
-                if field.is_required():
-                    base_field = base.model_fields.get(k)
-                    if not base_field:
-                        raise AttributeError(f'{k} not existed in {base.__name__}.')
-                    if base_field and base_field.annotation != field.annotation:
-                        raise AttributeError(f'type of {k} not consistent with {base.__name__}'  )
-            return  kls
-        return inner()
-    return wrap
-
-ensure_subset = ensure_subset_v2 if PYDANTIC_V2 else ensure_subset_v1
-
-
-def update_forward_refs(kls):
-    def update_pydantic_forward_refs(kls: Type[BaseModel]):
-        """
-        recursively update refs.
-        """
-        if getattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, False):
-            return
-
-        if PYDANTIC_V2:
-            kls.model_rebuild()
-        else:
-            kls.update_forward_refs()
-
-        setattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, True)
-
-        if PYDANTIC_V2:
-            values = kls.model_fields.values()
-        else:
-            values = kls.__fields__.values()
-
-        for field in values:
-            if PYDANTIC_V2:
-                shelled_type = shelling_type(field.annotation)
-            else:
-                shelled_type = shelling_type(field.type_)
-            update_forward_refs(shelled_type)
-
-    def update_dataclass_forward_refs(kls):
-        if not getattr(kls, const.DATACLASS_FORWARD_REF_UPDATED, False):
-            anno = get_type_hints(kls)
-            kls.__annotations__ = anno
-            setattr(kls, const.DATACLASS_FORWARD_REF_UPDATED, True)
-
-            for _, v in kls.__annotations__.items():
-                shelled_type = shelling_type(v)
-                update_forward_refs(shelled_type)
-
-    if safe_issubclass(kls, BaseModel):
-        update_pydantic_forward_refs(kls)
-
-    if is_dataclass(kls):
-        update_dataclass_forward_refs(kls)
-
-
-def try_parse_data_to_target_field_type_v1(target, field_name, data):
-    """
-    parse to pydantic or dataclass object
-    1. get type of target field
-    2. parse
-    """
-    field_type = None
-
-    # 1. get type of target field
-    if isinstance(target, BaseModel):
-        _fields = target.__class__.__fields__
-        field_type = _fields[field_name].outer_type_
-
-        # handle optional logic
-        if data is None and _fields[field_name].required == False:
-            return data
-
-    elif is_dataclass(target):
-        field_type = target.__class__.__annotations__[field_name]
-
-    # 2. parse
-    if field_type:
-        try:
-            result = parse_obj_as(field_type, data)
-            return result
-        except ValidationError as e:
-            print(f'Warning: type mismatch, pls check the return type for "{field_name}", expected: {field_type}')
-            raise e
-    else:
-        return data  #noqa
-
-def try_parse_data_to_target_field_type_v2(target, field_name, data):
-    """
-    parse to pydantic or dataclass object
-    1. get type of target field
-    2. parse
-    """
-    field_type = None
-
-    # 1. get type of target field
-    if isinstance(target, BaseModel):
-        _fields = target.__class__.model_fields
-        field_type = _fields[field_name].annotation
-
-        # handle optional logic
-        if data is None and _fields[field_name].is_required() == False:
-            return data
-
-    elif is_dataclass(target):
-        field_type = target.__class__.__annotations__[field_name]
-
-    # 2. parse
-    if field_type:
-        try:
-            # https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once
-            adapter = TypeAdapterManager.get(field_type)
-            result = adapter.validate_python(data)  
-            return result
-        except ValidationError as e:
-            print(f'Warning: type mismatch, pls check the return type for "{field_name}", expected: {field_type}')
-            raise e
-    
-    else:
-        return data  #noqa
-
-try_parse_data_to_target_field_type = try_parse_data_to_target_field_type_v2 if PYDANTIC_V2 else try_parse_data_to_target_field_type_v1
-
-def _is_optional(annotation):
-    annotation_origin = getattr(annotation, "__origin__", None)
-    return annotation_origin == Union \
-        and len(annotation.__args__) == 2 \
-        and annotation.__args__[1] == type(None)  # noqa
-
-def _is_list(annotation):
-    return getattr(annotation, "__origin__", None) == list
-
-def shelling_type(type):
-    while _is_optional(type) or _is_list(type):
-        type = type.__args__[0]
-    return type
-
-def get_kls_full_path(kls):
-    return f'{kls.__module__}.{kls.__qualname__}'
-
-def copy_dataloader_kls(name, loader_kls):
-    """
-    quickly copy from an existed DataLoader class
-    usage:
-    SeniorMemberLoader = copy_dataloader('SeniorMemberLoader', ul.UserByLevelLoader)
-    JuniorMemberLoader = copy_dataloader('JuniorMemberLoader', ul.UserByLevelLoader)
-    """
-    return type(name, loader_kls.__bases__, dict(loader_kls.__dict__))
-
-class StrictEmptyLoader(DataLoader):
-    async def batch_load_fn(self, keys):
-        """it should not be triggered, otherwise will raise Exception"""
-        raise ValueError('EmptyLoader should load from pre loaded data')
-
-class ListEmptyLoader(DataLoader):
-    async def batch_load_fn(self, keys):
-        dct = {}
-        return [dct.get(k, []) for k in keys]
-
-class SingleEmptyLoader(DataLoader):
-    async def batch_load_fn(self, keys):
-        dct = {}
-        return [dct.get(k, None) for k in keys]
-
-def generate_strict_empty_loader(name):
-    """generated Loader will raise ValueError if not found"""
-    return type(name, StrictEmptyLoader.__bases__, dict(StrictEmptyLoader.__dict__))  #noqa
-
-def generate_list_empty_loader(name):
-    """generated Loader will return [] if not found"""
-    return type(name, ListEmptyLoader.__bases__, dict(ListEmptyLoader.__dict__))  #noqa
-
-def generate_single_empty_loader(name):
-    """generated Loader will return None if not found"""
-    return type(name, SingleEmptyLoader.__bases__, dict(SingleEmptyLoader.__dict__))  #noqa
\ No newline at end of file
diff --git a/pydantic_resolve/utils/class_util.py b/pydantic_resolve/utils/class_util.py
new file mode 100644
index 0000000..9fdd576
--- /dev/null
+++ b/pydantic_resolve/utils/class_util.py
@@ -0,0 +1,186 @@
+import functools
+from dataclasses import is_dataclass
+from typing import Type, get_type_hints
+import pydantic_resolve.constant as const
+from pydantic_resolve.compat import PYDANTIC_V2
+import pydantic_resolve.utils.class_util as class_util
+from pydantic_resolve.analysis import is_acceptable_kls
+from pydantic_resolve.utils.types import shelling_type, get_type
+
+from pydantic import BaseModel
+
+
+def get_class_field_annotations(cls: Type):
+    anno = cls.__dict__.get('__annotations__') or {}
+    return anno.keys()
+
+
+def safe_issubclass(kls, classinfo):
+    try:
+        return issubclass(kls, classinfo)
+    except TypeError:
+        return False
+
+
+def rebuild_v1(kls):
+    kls.update_forward_refs()
+
+def rebuild_v2(kls):
+    kls.model_rebuild()
+
+rebuild = rebuild_v2 if PYDANTIC_V2 else rebuild_v1
+
+def update_forward_refs(kls):
+    def update_pydantic_forward_refs(kls: Type[BaseModel]):
+        """
+        recursively update refs.
+        """
+        if getattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, False):
+            return
+
+        rebuild(kls)
+
+        setattr(kls, const.PYDANTIC_FORWARD_REF_UPDATED, True)
+        
+        values = get_values(kls)
+
+        for field in values:
+            shelled_type = shelling_type(get_type(field))
+
+            update_forward_refs(shelled_type)
+
+    def update_dataclass_forward_refs(kls):
+        if not getattr(kls, const.DATACLASS_FORWARD_REF_UPDATED, False):
+            anno = get_type_hints(kls)
+            kls.__annotations__ = anno
+            setattr(kls, const.DATACLASS_FORWARD_REF_UPDATED, True)
+
+            for _, v in kls.__annotations__.items():
+                shelled_type = shelling_type(v)
+                update_forward_refs(shelled_type)
+
+    if safe_issubclass(kls, BaseModel):
+        update_pydantic_forward_refs(kls)
+
+    if is_dataclass(kls):
+        update_dataclass_forward_refs(kls)
+
+
+def ensure_subset_v1(base):
+    """
+    used with pydantic class to make sure a class's field is 
+    subset of target class
+    """
+    def wrap(kls):
+        assert safe_issubclass(base, BaseModel), 'base should be pydantic class'
+        assert safe_issubclass(kls, BaseModel), 'class should be pydantic class'
+
+        @functools.wraps(kls)
+        def inner():
+            for k, field in kls.__fields__.items():
+                if field.required:
+                    base_field = base.__fields__.get(k)
+                    if not base_field:
+                        raise AttributeError(f'{k} not existed in {base.__name__}.')
+                    if base_field and base_field.type_ != field.type_:
+                        raise AttributeError(f'type of {k} not consistent with {base.__name__}'  )
+            return kls
+        return inner()
+    return wrap
+
+
+def ensure_subset_v2(base):
+    """
+    used with pydantic class to make sure a class's field is 
+    subset of target class
+    """
+    def wrap(kls):
+        assert safe_issubclass(base, BaseModel), 'base should be pydantic class'
+        assert safe_issubclass(kls, BaseModel), 'class should be pydantic class'
+
+        @functools.wraps(kls)
+        def inner():
+            for k, field in kls.model_fields.items():
+                if field.is_required():
+                    base_field = base.model_fields.get(k)
+                    if not base_field:
+                        raise AttributeError(f'{k} not existed in {base.__name__}.')
+                    if base_field and base_field.annotation != field.annotation:
+                        raise AttributeError(f'type of {k} not consistent with {base.__name__}'  )
+            return  kls
+        return inner()
+    return wrap
+
+
+ensure_subset = ensure_subset_v2 if PYDANTIC_V2 else ensure_subset_v1
+
+
+def get_kls_full_path(kls):
+    return f'{kls.__module__}.{kls.__qualname__}'
+
+
+def _get_items_v1(kls):
+    return kls.__fields__.items()
+
+
+def _get_items_v2(kls):
+    return kls.model_fields.items()
+
+
+get_items = _get_items_v2 if PYDANTIC_V2 else _get_items_v1
+
+
+def _get_keys_v1(kls):
+    return kls.__fields__.keys()
+
+
+def _get_keys_v2(kls):
+    return kls.model_fields.keys()
+
+
+get_keys = _get_keys_v2 if PYDANTIC_V2 else _get_keys_v1
+
+
+def _get_values_v1(kls):
+    return kls.__fields__.values()
+
+
+def _get_values_v2(kls):
+    return kls.model_fields.values()
+
+
+get_values = _get_values_v2 if PYDANTIC_V2 else _get_values_v1
+
+
+def get_pydantic_attrs(kls):
+    items = class_util.get_items(kls)
+
+    for k, v in items:
+        t = get_type(v)
+
+        shelled_type = shelling_type(t)
+        if is_acceptable_kls(shelled_type):
+            yield (k, shelled_type)  # type_ is the most inner type
+
+
+def get_dataclass_attrs(kls):
+    for name, v in kls.__annotations__.items():
+        shelled_type = shelling_type(v)
+        if is_acceptable_kls(shelled_type):
+            yield (name, shelled_type)
+
+
+def get_class(target):
+    if isinstance(target, list):
+        return target[0].__class__
+    else:
+        return target.__class__
+
+
+def _is_required_v1(field):
+    return field.required
+
+def _is_required_v2(field):
+    return field.is_required()
+
+is_required_field = _is_required_v2 if PYDANTIC_V2 else _is_required_v1
\ No newline at end of file
diff --git a/pydantic_resolve/utils/collector.py b/pydantic_resolve/utils/collector.py
new file mode 100644
index 0000000..fbd5771
--- /dev/null
+++ b/pydantic_resolve/utils/collector.py
@@ -0,0 +1,34 @@
+import abc
+from typing import Any, List, Union
+
+class ICollector(metaclass=abc.ABCMeta):
+    @abc.abstractmethod
+    def __init__(self, alias: str):
+        self.alias = alias
+
+    @abc.abstractmethod
+    def add(self, val):
+        """how to add new element(s)"""
+
+    @abc.abstractmethod
+    def values(self) -> Any:
+        """get result"""
+
+
+class Collector(ICollector):
+    def __init__(self, alias: str, flat: bool=False):
+        super().__init__(alias)
+        self.flat = flat
+        self.val = []
+
+    def add(self, val: Union[Any, List[Any]]) -> None:
+        if self.flat:
+            if isinstance(val, list):
+                self.val.extend(val)
+            else:
+                raise TypeError('if flat, target should be list')
+        else:
+            self.val.append(val)
+
+    def values(self) -> List[Any]:
+        return self.val
\ No newline at end of file
diff --git a/pydantic_resolve/utils/conversion.py b/pydantic_resolve/utils/conversion.py
new file mode 100644
index 0000000..e4ff6b1
--- /dev/null
+++ b/pydantic_resolve/utils/conversion.py
@@ -0,0 +1,214 @@
+import asyncio
+import functools
+import types
+from inspect import iscoroutine
+from typing import Any, Callable, Optional, Type, Union
+from pydantic import BaseModel, ValidationError, parse_obj_as
+from dataclasses import is_dataclass
+from pydantic_resolve.compat import PYDANTIC_V2
+import pydantic_resolve.constant as const
+from pydantic_resolve.utils.class_util import safe_issubclass
+
+
+if PYDANTIC_V2:
+    from pydantic import BaseModel, TypeAdapter, ValidationError
+    class TypeAdapterManager:
+        apapters = {}
+        
+        @classmethod
+        def get(cls, type):
+            adapter = cls.apapters.get(type)
+            if adapter:
+                return adapter
+            else:
+                new_adapter = TypeAdapter(type)
+                cls.apapters[type] = new_adapter
+                return new_adapter
+
+def try_parse_data_to_target_field_type_v1(
+        target: object,
+        field_name: str,
+        data):
+    """
+    parse to pydantic or dataclass object
+    1. get type of target field
+    2. parse
+    """
+    field_type = None
+
+    # 1. get type of target field
+    if isinstance(target, BaseModel):
+        _fields = target.__class__.__fields__
+        field_type = _fields[field_name].outer_type_
+
+        # handle optional logic
+        if data is None and _fields[field_name].required == False:
+            return data
+
+    elif is_dataclass(target):
+        field_type = target.__class__.__annotations__[field_name]
+
+    # 2. parse
+    if field_type:
+        try:
+            result = parse_obj_as(field_type, data)
+            return result
+        except ValidationError as e:
+            print(f'Warning: type mismatch, pls check the return type for "{field_name}", expected: {field_type}')
+            raise e
+    else:
+        return data  #noqa
+
+
+def try_parse_data_to_target_field_type_v2(
+        target: object,
+        field_name: str,
+        data):
+    """
+    parse to pydantic or dataclass object
+    1. get type of target field
+    2. parse
+    """
+    field_type = None
+
+    # 1. get type of target field
+    if isinstance(target, BaseModel):
+        _fields = target.__class__.model_fields
+        field_type = _fields[field_name].annotation
+
+        # handle optional logic
+        if data is None and _fields[field_name].is_required() == False:
+            return data
+
+    elif is_dataclass(target):
+        field_type = target.__class__.__annotations__[field_name]
+
+    # 2. parse
+    if field_type:
+        try:
+            # https://docs.pydantic.dev/latest/concepts/performance/#typeadapter-instantiated-once
+            adapter = TypeAdapterManager.get(field_type)
+            result = adapter.validate_python(data)
+            return result
+        except ValidationError as e:
+            print(f'Warning: type mismatch, pls check the return type for "{field_name}", expected: {field_type}')
+            raise e
+
+    else:
+        return data  #noqa
+
+
+try_parse_data_to_target_field_type = try_parse_data_to_target_field_type_v2 if PYDANTIC_V2 else try_parse_data_to_target_field_type_v1
+
+
+def _get_mapping_rule_v1(target, source) -> Optional[Callable]:
+    # do noting
+    if isinstance(source, target):
+        return None
+
+    # pydantic
+    if safe_issubclass(target, BaseModel):
+        if target.__config__.orm_mode:
+            if isinstance(source, dict):
+                raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic from_orm can't handle dict object")
+            else:
+                return lambda t, s: t.from_orm(s)
+
+        if isinstance(source, (dict, BaseModel)):
+            return lambda t, s: t.parse_obj(s)
+
+        else:
+            raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic can't handle non-dict data")
+
+    # dataclass
+    if is_dataclass(target):
+        if isinstance(source, dict):
+            return lambda t, s: t(**s)
+
+    raise NotImplementedError(f"{type(source)} -> {target.__name__}: faild to get auto mapping rule and execut mapping, use your own rule instead.")
+
+
+def _get_mapping_rule_v2(target, source) -> Optional[Callable]:
+    # do noting
+    if isinstance(source, target):
+        return None
+
+    # pydantic
+    if safe_issubclass(target, BaseModel):
+        if target.model_config.get('from_attributes'):
+            if isinstance(source, dict):
+                raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic from_orm can't handle dict object")
+            else:
+                return lambda t, s: t.model_validate(s)
+
+        if isinstance(source, dict):
+            return lambda t, s: t.model_validate(s)
+
+        if isinstance(source, BaseModel):
+            if source.model_config.get('from_attributes'):
+                return lambda t, s: t.model_validate(s)
+            else:
+                return lambda t, s: t(**s.model_dump())
+
+        else:
+            raise AttributeError(f"{type(source)} -> {target.__name__}: pydantic can't handle non-dict data")
+
+    # dataclass
+    if is_dataclass(target):
+        if isinstance(source, dict):
+            return lambda t, s: t(**s)
+
+    raise NotImplementedError(f"{type(source)} -> {target.__name__}: faild to get auto mapping rule and execut mapping, use your own rule instead.")
+
+
+_get_mapping_rule = _get_mapping_rule_v2 if PYDANTIC_V2 else _get_mapping_rule_v1
+
+
+def _apply_rule(rule: Optional[Callable], target, source: Any, is_list: bool):
+    if not rule:  # no change
+        return source
+
+    if is_list:
+        return [rule(target, s) for s in source]
+    else:
+        return rule(target, source)
+
+
+def mapper(func_or_class: Union[Callable, Type]):
+    """
+    execute post-transform function after the value is reolved
+    func_or_class:
+        is func: run func
+        is class: call auto_mapping to have a try
+    """
+    def inner(inner_fn):
+
+        # if mapper provided, auto map from target type will be disabled
+        setattr(inner_fn, const.HAS_MAPPER_FUNCTION, True)
+
+        @functools.wraps(inner_fn)
+        async def wrap(*args, **kwargs):
+
+            retVal = inner_fn(*args, **kwargs)
+            while iscoroutine(retVal) or asyncio.isfuture(retVal):
+                retVal = await retVal  # get final result
+
+            if retVal is None:
+                return None
+
+            if isinstance(func_or_class, types.FunctionType):
+                # manual mapping
+                return func_or_class(retVal)
+            else:
+                # auto mapping
+                if isinstance(retVal, list):
+                    if retVal:
+                        rule = _get_mapping_rule(func_or_class, retVal[0])
+                        return _apply_rule(rule, func_or_class, retVal, True)
+                    else:
+                        return retVal  # return []
+                else:
+                    rule = _get_mapping_rule(func_or_class, retVal)
+                    return _apply_rule(rule, func_or_class, retVal, False)
+        return wrap
+    return inner
\ No newline at end of file
diff --git a/pydantic_resolve/utils/dataloader.py b/pydantic_resolve/utils/dataloader.py
new file mode 100644
index 0000000..c659e67
--- /dev/null
+++ b/pydantic_resolve/utils/dataloader.py
@@ -0,0 +1,72 @@
+from collections import defaultdict
+from typing import Callable, DefaultDict, Iterator, List, Mapping, Optional, Sequence, TypeVar
+from aiodataloader import DataLoader
+
+T = TypeVar("T")
+V = TypeVar("V")
+
+def build_list(items: Sequence[T], keys: List[V], get_pk: Callable[[T], V]) -> Iterator[List[T]]:
+    """
+    helper function to build return list data required by aiodataloader
+    """
+    dct: DefaultDict[V, List[T]] = defaultdict(list)
+    for item in items:
+        _key = get_pk(item)
+        dct[_key].append(item)
+    results = (dct.get(k, []) for k in keys)
+    return results
+
+
+def build_object(items: Sequence[T], keys: List[V], get_pk: Callable[[T], V]) -> Iterator[Optional[T]]:
+    """
+    helper function to build return object data required by aiodataloader
+    """
+    dct: Mapping[V, T] = {}
+    for item in items:
+        _key = get_pk(item)
+        dct[_key] = item
+    results = (dct.get(k, None) for k in keys)
+    return results
+
+
+def copy_dataloader_kls(name, loader_kls):
+    """
+    quickly copy from an existed DataLoader class
+    usage:
+    SeniorMemberLoader = copy_dataloader('SeniorMemberLoader', ul.UserByLevelLoader)
+    JuniorMemberLoader = copy_dataloader('JuniorMemberLoader', ul.UserByLevelLoader)
+    """
+    return type(name, loader_kls.__bases__, dict(loader_kls.__dict__))
+
+
+class StrictEmptyLoader(DataLoader):
+    async def batch_load_fn(self, keys):
+        """it should not be triggered, otherwise will raise Exception"""
+        raise ValueError('EmptyLoader should load from pre loaded data')
+
+
+class ListEmptyLoader(DataLoader):
+    async def batch_load_fn(self, keys):
+        dct = {}
+        return [dct.get(k, []) for k in keys]
+
+
+class SingleEmptyLoader(DataLoader):
+    async def batch_load_fn(self, keys):
+        dct = {}
+        return [dct.get(k, None) for k in keys]
+
+
+def generate_strict_empty_loader(name):
+    """generated Loader will raise ValueError if not found"""
+    return type(name, StrictEmptyLoader.__bases__, dict(StrictEmptyLoader.__dict__))  #noqa
+
+
+def generate_list_empty_loader(name):
+    """generated Loader will return [] if not found"""
+    return type(name, ListEmptyLoader.__bases__, dict(ListEmptyLoader.__dict__))  #noqa
+
+
+def generate_single_empty_loader(name):
+    """generated Loader will return None if not found"""
+    return type(name, SingleEmptyLoader.__bases__, dict(SingleEmptyLoader.__dict__))  #noqa
\ No newline at end of file
diff --git a/pydantic_resolve/utils/depend.py b/pydantic_resolve/utils/depend.py
new file mode 100644
index 0000000..4ca87b1
--- /dev/null
+++ b/pydantic_resolve/utils/depend.py
@@ -0,0 +1,14 @@
+from typing import Any, Callable, Optional
+
+class Depends:
+    def __init__(
+        self,
+        dependency: Optional[Callable[..., Any]] = None,
+    ):
+        self.dependency = dependency
+
+
+def LoaderDepend(  # noqa: N802
+    dependency: Optional[Callable[..., Any]] = None,
+) -> Any:
+    return Depends(dependency=dependency)
\ No newline at end of file
diff --git a/pydantic_resolve/utils/experiment.py b/pydantic_resolve/utils/experiment.py
new file mode 100644
index 0000000..91bca5c
--- /dev/null
+++ b/pydantic_resolve/utils/experiment.py
@@ -0,0 +1,7 @@
+from typing import Callable, Type
+
+
+def replace_method(cls: Type, cls_name: str, func_name: str, func: Callable):
+    """test-only"""
+    KLS = type(cls_name, (cls,), {func_name: func})
+    return KLS
\ No newline at end of file
diff --git a/pydantic_resolve/utils/openapi.py b/pydantic_resolve/utils/openapi.py
new file mode 100644
index 0000000..6e54a52
--- /dev/null
+++ b/pydantic_resolve/utils/openapi.py
@@ -0,0 +1,107 @@
+from pydantic import BaseModel
+from typing import Any, Dict
+import pydantic_resolve.constant as const
+from pydantic_resolve.compat import PYDANTIC_V2
+from inspect import isfunction
+from pydantic_resolve.utils.class_util import safe_issubclass, is_required_field, get_items
+
+
+def _get_required_fields(kls: BaseModel):
+    required_fields = []
+
+    items = get_items(kls)
+
+    for fname, field in items:
+        if is_required_field(field):
+            required_fields.append(fname)
+
+
+    # 2. get resolve_ and post_ target fields
+    for f in dir(kls):
+        if f.startswith(const.RESOLVE_PREFIX):
+            if isfunction(getattr(kls, f)):
+                required_fields.append(f.replace(const.RESOLVE_PREFIX, ''))
+
+        if f.startswith(const.POST_PREFIX):
+            if isfunction(getattr(kls, f)):
+                required_fields.append(f.replace(const.POST_PREFIX, ''))
+
+    return required_fields
+
+
+def model_config_v1(default_required: bool = True):
+    """
+    - default_required: 
+        if resolve field has default value, it will not be listed in schema['required']
+        set default_required=True to add it into required list.
+    """
+    def wrapper(kls):
+        if safe_issubclass(kls, BaseModel):
+
+            # override schema_extra method
+            def _schema_extra(schema: Dict[str, Any], model) -> None:
+                # define schema.properties
+                excludes = set()
+
+                if kls.__exclude_fields__:
+                    for k in kls.__exclude_fields__.keys():
+                        excludes.add(k)
+
+                props = {}
+                for k, v in schema.get('properties', {}).items():
+                    if k not in excludes:
+                        props[k] = v
+                schema['properties'] = props
+
+                # define schema.required
+                if default_required:
+                    fnames = _get_required_fields(model)
+                    schema['required'] = fnames
+            kls.__config__.schema_extra = staticmethod(_schema_extra)
+        else:
+            raise AttributeError(f'target class {kls.__name__} is not BaseModel')
+        return kls
+    return wrapper
+
+
+def model_config_v2(default_required: bool=True):
+    """
+    in pydantic v2, we can not use __exclude_field__ to set hidden field in model_config hidden_field params
+    model_config now is just a simple decorator to remove fields (with exclude=True) from schema.properties
+    and set schema.required for better schema description. 
+    (same like `output` decorator, you can replace output with model_config)
+
+    it keeps the form of model_config(params) in order to extend new features in future
+    """
+    def wrapper(kls):
+        if safe_issubclass(kls, BaseModel):
+            def build():
+                def _schema_extra(schema: Dict[str, Any], model) -> None:
+                    # 1. collect exclude fields and then hide in both schema and dump (default action)
+                    excluded_fields = [k for k, v in kls.model_fields.items() if v.exclude == True]
+                    props = {}
+
+                    # config schema properties
+                    for k, v in schema.get('properties', {}).items():
+                        if k not in excluded_fields:
+                            props[k] = v
+                    schema['properties'] = props
+
+                    # config schema required (fields with default values will not be listed in required field)
+                    # and the generated typescript models will define it as optional, and is troublesome in use
+                    if default_required:
+                        fnames = _get_required_fields(model)
+                        if excluded_fields:
+                            fnames = [n for n in fnames if n not in excluded_fields]
+                        schema['required'] = fnames
+
+                return _schema_extra
+
+            kls.model_config['json_schema_extra'] = staticmethod(build())
+        else:
+            raise AttributeError(f'target class {kls.__name__} is not BaseModel')
+        return kls
+    return wrapper
+
+
+model_config = model_config_v2 if PYDANTIC_V2 else model_config_v1
\ No newline at end of file
diff --git a/pydantic_resolve/utils/params.py b/pydantic_resolve/utils/params.py
new file mode 100644
index 0000000..05a6285
--- /dev/null
+++ b/pydantic_resolve/utils/params.py
@@ -0,0 +1,10 @@
+from typing import Any, Dict
+from pydantic_resolve.exceptions import GlobalLoaderFieldOverlappedError
+
+
+def merge_dicts(a: Dict[str, Any], b: Dict[str, Any]):
+    overlap = set(a.keys()) & set(b.keys())
+    if overlap:
+        raise GlobalLoaderFieldOverlappedError(f'loader_params and global_loader_param have duplicated key(s): {",".join(overlap)}')
+    else:
+        return {**a, **b}
\ No newline at end of file
diff --git a/pydantic_resolve/utils/types.py b/pydantic_resolve/utils/types.py
new file mode 100644
index 0000000..aabf987
--- /dev/null
+++ b/pydantic_resolve/utils/types.py
@@ -0,0 +1,35 @@
+from typing import Type, Union
+from pydantic_resolve.compat import PYDANTIC_V2
+
+
+def _is_optional(annotation):
+    annotation_origin = getattr(annotation, "__origin__", None)
+    return annotation_origin == Union \
+        and len(annotation.__args__) == 2 \
+        and annotation.__args__[1] == type(None)  # noqa
+
+
+def _is_list(annotation):
+    return getattr(annotation, "__origin__", None) == list
+
+
+def shelling_type(type):
+    while _is_optional(type) or _is_list(type):
+        type = type.__args__[0]
+    return type
+
+
+def get_class_field_annotations(cls: Type):
+    anno = cls.__dict__.get('__annotations__') or {}
+    return anno.keys()
+
+
+def _get_type_v1(v):
+    return v.type_
+
+
+def _get_type_v2(v):
+    return v.annotation
+
+
+get_type = _get_type_v2 if PYDANTIC_V2 else _get_type_v1
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 53098dc..217ac1e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "pydantic-resolve"
-version = "1.11.0"
+version = "1.11.1"
 description = "It just provide a pair of pre & post methods around pydantic fields, the rest is up to your imagination"
 authors = ["tangkikodo <allmonday@126.com>"]
 readme = "README.md"
diff --git a/research/5_pydantic.py b/research/5_pydantic.py
index 84bd333..a0d2291 100644
--- a/research/5_pydantic.py
+++ b/research/5_pydantic.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 from typing import List, Optional
 from pydantic import BaseModel
-from pydantic_resolve.util import update_forward_refs
+from pydantic_resolve.utils.class_util import update_forward_refs
 
 class ClassRoom(BaseModel):
     students: List[Student]
diff --git a/tox.ini b/tox.ini
index 38756eb..d0b865d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,6 +11,12 @@ setenv = VIRTUALENV_DISCOVERY=pyenv
 [testenv]
 allowlist_externals = poetry
 
+[testenv:py38-duration]
+basepython = python3.8
+commands_pre =
+    poetry install --no-root --sync
+commands =
+  poetry run pytest --durations=20 tests/pydantic_v1
 
 [testenv:py38pyd1]
 basepython = python3.8


Test patch:
diff --git a/research/parse_obj_as_test.py b/research/parse_obj_as_test.py
new file mode 100644
index 0000000..351bfa6
--- /dev/null
+++ b/research/parse_obj_as_test.py
@@ -0,0 +1,29 @@
+from pydantic import parse_obj_as, BaseModel
+from timeit import timeit
+
+class Y(BaseModel):
+    name: str
+
+class X(BaseModel):
+    name: str
+    y: Y
+
+class A(BaseModel):
+    name: str
+    x: X
+
+a = A(name='a', x=X(name='x', y=Y(name='y')))
+
+def test_a():
+    parse_obj_as(A, {'name': 'a', 'x': {'name': 'x', 'y': {'name': 'y'}}})
+
+
+def test_b():
+    parse_obj_as(A, a)
+
+print(timeit(test_a, number=100000))
+print(timeit(test_b, number=100000))
+
+
+b = parse_obj_as(A, a)
+print(id(b.x) == id(a.x))
\ No newline at end of file
diff --git a/tests/pydantic_v1/core/test_field.py b/tests/pydantic_v1/core/test_field.py
index 4ccbd74..5f7b1c8 100644
--- a/tests/pydantic_v1/core/test_field.py
+++ b/tests/pydantic_v1/core/test_field.py
@@ -1,6 +1,6 @@
 # from __future__ import annotations
 from pydantic import BaseModel
-from pydantic_resolve.core import get_class
+from pydantic_resolve.utils.class_util import get_class
 
 def test_get_class():
     class Student(BaseModel):
diff --git a/tests/pydantic_v1/core/test_field_dataclass.py b/tests/pydantic_v1/core/test_field_dataclass.py
index adf4e59..17480f8 100644
--- a/tests/pydantic_v1/core/test_field_dataclass.py
+++ b/tests/pydantic_v1/core/test_field_dataclass.py
@@ -1,7 +1,7 @@
 # from __future__ import annotations
 from dataclasses import dataclass, field
 from typing import Optional, List
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 
 @dataclass
 class Queue:
@@ -44,7 +44,7 @@ def test_get_all_fields():
         'test_field_dataclass.Student': {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name'],
-            'attribute': ['zone', 'zeta2', 'zetas2'],
+            'object_fields': ['zone', 'zeta2', 'zetas2'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {},
             'has_context': False,
@@ -52,7 +52,7 @@ def test_get_all_fields():
         'test_field_dataclass.Zone': {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {},
             'has_context': False,
@@ -60,7 +60,7 @@ def test_get_all_fields():
         'test_field_dataclass.Queue': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'has_context': False,
@@ -68,7 +68,7 @@ def test_get_all_fields():
         'test_field_dataclass.Zeta': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'has_context': False,
diff --git a/tests/pydantic_v1/core/test_field_dataclass_anno.py b/tests/pydantic_v1/core/test_field_dataclass_anno.py
index 56e26b9..388d1ef 100644
--- a/tests/pydantic_v1/core/test_field_dataclass_anno.py
+++ b/tests/pydantic_v1/core/test_field_dataclass_anno.py
@@ -1,16 +1,18 @@
+from __future__ import annotations
 from dataclasses import dataclass, field
 from typing import Optional, List
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 from pydantic_resolve import LoaderDepend, Collector
 
 
 async def loader_fn(keys):
     return keys
 
+
 @dataclass
 class Student:
     __pydantic_resolve_expose__ = {'name': 'student_name'}
-    zone: Optional['Zone'] = None
+    zone: Optional[Zone] = None
 
     name: str = ''
     def resolve_name(self, context, ancestor_context, loader=LoaderDepend(loader_fn)):
@@ -19,7 +21,7 @@ def resolve_name(self, context, ancestor_context, loader=LoaderDepend(loader_fn)
     def post_name(self):
         return '.'
 
-    zeta: Optional['Zeta'] = None
+    zeta: Optional[Zeta] = None
 
     def resolve_zeta(self):
         return dict(name='z')
@@ -27,7 +29,6 @@ def resolve_zeta(self):
     queue_names: List[str] = field(default_factory=list)
     def post_queue_names(self, collector=Collector('queue_name')):
         return collector.values()
-    
 
 @dataclass
 class Queue:
@@ -45,20 +46,45 @@ class Zeta:
 
 
 def test_get_all_fields():
+    """
+    post params is not included
+    """
     result = scan_and_store_metadata(Student)
     expect = {
         'test_field_dataclass_anno.Student': {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name', 'post_queue_names'],
-            'attribute': ['zone'],
+            'object_fields': ['zone'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {},
-            'kls': Student
+            'kls': Student,
+            'resolve_params': {
+                'resolve_name': {
+                    'trim_field': 'name',
+                    'context': True,
+                    'ancestor_context': True,
+                    'parent': False,
+                    'dataloaders': [
+                        {
+                            'param': 'loader',
+                            'kls': loader_fn,
+                            'path': 'test_field_dataclass_anno.loader_fn'
+                        }
+                    ],
+                }, 
+                'resolve_zeta': {
+                    'trim_field': 'zeta',
+                    'context': False,
+                    'ancestor_context': False,
+                    'parent': False,
+                    'dataloaders': [],
+                }
+            },
         },
         'test_field_dataclass_anno.Zone': {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zone
@@ -66,7 +92,7 @@ def test_get_all_fields():
         'test_field_dataclass_anno.Queue': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {'name': 'queue_name'},
             'kls': Queue
@@ -74,7 +100,7 @@ def test_get_all_fields():
         'test_field_dataclass_anno.Zeta': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zeta
@@ -82,34 +108,3 @@ def test_get_all_fields():
     }
     for k, v in result.items():
         assert expect[k].items() <= v.items()
-
-def test_resolve_params():
-    result = scan_and_store_metadata(Student)
-    expect = {
-        'test_field_dataclass_anno.Student': {
-            'resolve_params': {
-                'resolve_name': {
-                    'trim_field': 'name',
-                    'context': True,
-                    'ancestor_context': True,
-                    'parent': False,
-                    'dataloaders': [
-                        {
-                            'param': 'loader',
-                            'kls': loader_fn,
-                            'path': 'test_field_dataclass_anno.loader_fn'
-                        }
-                    ],
-                }, 
-                'resolve_zeta': {
-                    'trim_field': 'zeta',
-                    'context': False,
-                    'ancestor_context': False,
-                    'parent': False,
-                    'dataloaders': [],
-                }
-            },
-        },
-    }
-    key = 'test_field_dataclass_anno.Student'
-    assert expect[key].items() <= result[key].items()
\ No newline at end of file
diff --git a/tests/pydantic_v1/core/test_field_mix.py b/tests/pydantic_v1/core/test_field_mix.py
index 645a41b..62bbcf8 100644
--- a/tests/pydantic_v1/core/test_field_mix.py
+++ b/tests/pydantic_v1/core/test_field_mix.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass
 from typing import Optional, List
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 
 @dataclass
 class Book:
@@ -24,14 +24,14 @@ def test_get_all_fields():
         'test_field_mix.Student': {
             'resolve': ['resolve_books'],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {'name': 's_name'},
             'collect_dict': {}
         },
         'test_field_mix.Book': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
         },
diff --git a/tests/pydantic_v1/core/test_field_pydantic.py b/tests/pydantic_v1/core/test_field_pydantic.py
index 4bd734d..52e57d0 100644
--- a/tests/pydantic_v1/core/test_field_pydantic.py
+++ b/tests/pydantic_v1/core/test_field_pydantic.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 from typing import Optional, List
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata, convert_metadata_key_as_kls
+from pydantic_resolve.analysis import scan_and_store_metadata, convert_metadata_key_as_kls
 from pydantic_resolve import LoaderDepend
 
 async def loader_fn(keys):
@@ -45,7 +45,7 @@ def test_get_all_fields():
         'test_field_pydantic.Student': {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name'],
-            'attribute': ['zones', 'zones2', 'zone'],
+            'object_fields': ['zones', 'zones2', 'zone'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {}
             # ... others
@@ -53,7 +53,7 @@ def test_get_all_fields():
         'test_field_pydantic.Zone': {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
@@ -61,7 +61,7 @@ def test_get_all_fields():
         'test_field_pydantic.Queue': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
@@ -69,7 +69,7 @@ def test_get_all_fields():
         'test_field_pydantic.Zeta': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
@@ -86,7 +86,7 @@ def test_convert_metadata():
         Student: {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name'],
-            'attribute': ['zones', 'zones2', 'zone'],
+            'object_fields': ['zones', 'zones2', 'zone'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {},
             'kls': Student,
@@ -96,7 +96,7 @@ def test_convert_metadata():
         Zone: {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zone,
@@ -106,7 +106,7 @@ def test_convert_metadata():
         Queue: {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Queue,
@@ -116,7 +116,7 @@ def test_convert_metadata():
         Zeta: {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zeta,
diff --git a/tests/pydantic_v1/core/test_field_pydantic_error.py b/tests/pydantic_v1/core/test_field_pydantic_error.py
index 6d14eb9..07c34a1 100644
--- a/tests/pydantic_v1/core/test_field_pydantic_error.py
+++ b/tests/pydantic_v1/core/test_field_pydantic_error.py
@@ -2,7 +2,7 @@
 import pytest
 from typing import Optional
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 
 class A(BaseModel):
     __pydantic_resolve_expose__ = {'name': 'A_name'}
diff --git a/tests/pydantic_v1/core/test_field_validate_and_create_loader_instance.py b/tests/pydantic_v1/core/test_field_validate_and_create_loader_instance.py
index 969742f..70dcde6 100644
--- a/tests/pydantic_v1/core/test_field_validate_and_create_loader_instance.py
+++ b/tests/pydantic_v1/core/test_field_validate_and_create_loader_instance.py
@@ -3,7 +3,7 @@
 from typing import Optional, List
 from pydantic import BaseModel
 from aiodataloader import DataLoader
-from pydantic_resolve.core import scan_and_store_metadata, validate_and_create_loader_instance
+from pydantic_resolve.analysis import scan_and_store_metadata, validate_and_create_loader_instance
 from pydantic_resolve import LoaderDepend, LoaderFieldNotProvidedError
 
 async def loader_fn(keys):
diff --git a/tests/pydantic_v1/core/test_scan_post_method.py b/tests/pydantic_v1/core/test_scan_post_method.py
index 9c8912f..5cf5cf4 100644
--- a/tests/pydantic_v1/core/test_scan_post_method.py
+++ b/tests/pydantic_v1/core/test_scan_post_method.py
@@ -1,5 +1,5 @@
 from pydantic import BaseModel
-from pydantic_resolve.core import _scan_post_method, _scan_post_default_handler
+from pydantic_resolve.analysis import _scan_post_method, _scan_post_default_handler
 from pydantic_resolve import Collector
 
 def test_scan_post_method_1():
@@ -37,19 +37,30 @@ def post_a(self, context, ancestor_context, parent):
 
 
 def test_scan_post_method_3():
+    """
+    test signature of kls, field_name and collector_name
+    """
     class A(BaseModel):
         a: str
-        def post_a(self, context, ancestor_context, collector=Collector(alias='c_name')):
+        def post_a(self, 
+                   context,
+                   ancestor_context,
+                   collector=Collector(alias='c_name'),
+                   collector_2=Collector(alias='c_name')):
             return 2 * self.a
         
     result = _scan_post_method(A.post_a, 'post_a')
 
-    assert len(result['collectors']) == 1
+    assert len(result['collectors']) == 2
     assert result['collectors'][0]['field'] == 'post_a' 
     assert result['collectors'][0]['param'] == 'collector' 
     assert result['collectors'][0]['alias'] == 'c_name'
     assert isinstance(result['collectors'][0]['instance'], Collector)
 
+    assert result['collectors'][1]['field'] == 'post_a' 
+    assert result['collectors'][1]['param'] == 'collector_2' 
+    assert result['collectors'][1]['alias'] == 'c_name'
+    assert isinstance(result['collectors'][1]['instance'], Collector)
 
 def test_scan_post_method_4():
     class A(BaseModel):
diff --git a/tests/pydantic_v1/core/test_scan_resolve_method.py b/tests/pydantic_v1/core/test_scan_resolve_method.py
index c63ef43..4e180a6 100644
--- a/tests/pydantic_v1/core/test_scan_resolve_method.py
+++ b/tests/pydantic_v1/core/test_scan_resolve_method.py
@@ -1,7 +1,8 @@
 import pytest
 from aiodataloader import DataLoader
 from pydantic import BaseModel
-from pydantic_resolve.core import _scan_resolve_method, LoaderDepend, Collector
+from pydantic_resolve.analysis import _scan_resolve_method
+from pydantic_resolve import LoaderDepend, Collector
 
 def test_scan_resolve_method_1():
     class A(BaseModel):
diff --git a/tests/pydantic_v1/core/test_specific_type.py b/tests/pydantic_v1/core/test_specific_type.py
index 2ab8cf9..4db878f 100644
--- a/tests/pydantic_v1/core/test_specific_type.py
+++ b/tests/pydantic_v1/core/test_specific_type.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 from typing import Tuple
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata, convert_metadata_key_as_kls
+from pydantic_resolve.analysis import scan_and_store_metadata, convert_metadata_key_as_kls
 from pydantic_resolve import LoaderDepend
 
 async def loader_fn(keys):
@@ -17,7 +17,7 @@ def test_get_all_fields():
         'test_specific_type.Student': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
diff --git a/tests/pydantic_v1/resolver/test_16_mapper_1.py b/tests/pydantic_v1/resolver/test_16_mapper_1.py
index 7bdbf3f..5e5fbf1 100644
--- a/tests/pydantic_v1/resolver/test_16_mapper_1.py
+++ b/tests/pydantic_v1/resolver/test_16_mapper_1.py
@@ -1,5 +1,4 @@
 from __future__ import annotations
-from dataclasses import dataclass
 from typing import List
 import pytest
 from pydantic import BaseModel
diff --git a/tests/pydantic_v1/resolver/test_16_mapper_6.py b/tests/pydantic_v1/resolver/test_16_mapper_6.py
index b5239ee..91eb315 100644
--- a/tests/pydantic_v1/resolver/test_16_mapper_6.py
+++ b/tests/pydantic_v1/resolver/test_16_mapper_6.py
@@ -1,10 +1,8 @@
 from __future__ import annotations
-from dataclasses import dataclass
 from typing import List
 import pytest
 from pydantic import BaseModel
 from pydantic_resolve import Resolver, LoaderDepend, mapper
-from aiodataloader import DataLoader
 
 class Bo(BaseModel):
     name: str
diff --git a/tests/pydantic_v1/resolver/test_1_pydantic_resolve.py b/tests/pydantic_v1/resolver/test_1_pydantic_resolve.py
index bc5af70..355c78b 100644
--- a/tests/pydantic_v1/resolver/test_1_pydantic_resolve.py
+++ b/tests/pydantic_v1/resolver/test_1_pydantic_resolve.py
@@ -13,7 +13,7 @@ def resolve_intro(self) -> str:
 
     books: List[str] = []    
     async def resolve_books(self) -> List[str]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(0.1)
         return ['book1', 'book2']
 
 
diff --git a/tests/pydantic_v1/resolver/test_21_not_stop_by_idle_level.py b/tests/pydantic_v1/resolver/test_21_not_stop_by_idle_level.py
index 9f52c0c..53319d7 100644
--- a/tests/pydantic_v1/resolver/test_21_not_stop_by_idle_level.py
+++ b/tests/pydantic_v1/resolver/test_21_not_stop_by_idle_level.py
@@ -19,7 +19,7 @@ class B(BaseModel):
     name: str
     c: Optional[C] = None
     async def resolve_c(self) -> Optional[C]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return C(name='hello world')
 
 class A(BaseModel):
diff --git a/tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_1.py b/tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_1.py
index 53d9ec8..627909c 100644
--- a/tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_1.py
+++ b/tests/pydantic_v1/resolver/test_22_not_stop_by_idle_level_complex_1.py
@@ -1,4 +1,3 @@
-from dataclasses import asdict, dataclass, field
 from typing import List
 import pytest
 from pydantic import BaseModel
diff --git a/tests/pydantic_v1/resolver/test_26_tree.py b/tests/pydantic_v1/resolver/test_26_tree.py
index 9331cb6..a3577da 100644
--- a/tests/pydantic_v1/resolver/test_26_tree.py
+++ b/tests/pydantic_v1/resolver/test_26_tree.py
@@ -4,7 +4,7 @@
 from typing import List
 from pydantic import BaseModel
 from pydantic_resolve import Resolver, LoaderDepend
-from pydantic_resolve.util import generate_list_empty_loader
+from pydantic_resolve.utils.dataloader import generate_list_empty_loader
 import pytest
 
 
diff --git a/tests/pydantic_v1/resolver/test_27_context.py b/tests/pydantic_v1/resolver/test_27_context.py
index 9045e4c..ad00cf5 100644
--- a/tests/pydantic_v1/resolver/test_27_context.py
+++ b/tests/pydantic_v1/resolver/test_27_context.py
@@ -10,7 +10,7 @@ class Human(BaseModel):
     lucky: bool = True
     async def resolve_lucky(self):
         print('calculating...')
-        await asyncio.sleep(1)  # mock i/o
+        await asyncio.sleep(0.1)  # mock i/o
         return random() > 0.5
     
 class Earth(BaseModel):
diff --git a/tests/pydantic_v1/resolver/test_2_resolve_object.py b/tests/pydantic_v1/resolver/test_2_resolve_object.py
index 1dd17b6..250b580 100644
--- a/tests/pydantic_v1/resolver/test_2_resolve_object.py
+++ b/tests/pydantic_v1/resolver/test_2_resolve_object.py
@@ -12,39 +12,39 @@ class ServiceDetail1(BaseModel):
     detail_a: Optional[DetailA] = None
 
     async def resolve_detail_a(self) -> Optional[DetailA]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return DetailA(name='hello world')
 
     detail_b: str = ''
 
     async def resolve_detail_b(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return 'good'
 
 class Service(BaseModel):
     service_detail_1: Optional[ServiceDetail1] = None
     async def resolve_service_detail_1(self) -> Optional[ServiceDetail1]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ServiceDetail1()
 
     service_detail_1b: Optional[ServiceDetail1] = None
     async def resolve_service_detail_1b(self) -> Optional[ServiceDetail1]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ServiceDetail1()
 
     service_detail_2: str = ''
     async def resolve_service_detail_2(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return "detail_2"
 
     service_detail_3: str = ''
     async def resolve_service_detail_3(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return "detail_3"
 
     service_detail_4: str = ''
     async def resolve_service_detail_4(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return "detail_4"
 
 @pytest.mark.asyncio
diff --git a/tests/pydantic_v1/resolver/test_35_collector.py b/tests/pydantic_v1/resolver/test_35_collector.py
index ac0b796..9817e5f 100644
--- a/tests/pydantic_v1/resolver/test_35_collector.py
+++ b/tests/pydantic_v1/resolver/test_35_collector.py
@@ -1,10 +1,9 @@
 from __future__ import annotations
 from pydantic import BaseModel
 from typing import List
-from pydantic_resolve import Resolver, Collector
+from pydantic_resolve import Resolver, Collector, LoaderDepend
 import pytest
 
-from pydantic_resolve.core import LoaderDepend
 
 class SubCollector(Collector):
     def add(self, val):  # replace with your implementation
diff --git a/tests/pydantic_v1/resolver/test_39_post_async.py b/tests/pydantic_v1/resolver/test_39_post_async.py
index dce45b4..7d10064 100644
--- a/tests/pydantic_v1/resolver/test_39_post_async.py
+++ b/tests/pydantic_v1/resolver/test_39_post_async.py
@@ -9,7 +9,7 @@ class A(BaseModel):
 
     greet: str = ''
     async def post_greet(self):
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return 'hello ' + self.name
 
 @pytest.mark.asyncio
diff --git a/tests/pydantic_v1/resolver/test_3_tuple_list.py b/tests/pydantic_v1/resolver/test_3_tuple_list.py
index b9ae427..2a4cae2 100644
--- a/tests/pydantic_v1/resolver/test_3_tuple_list.py
+++ b/tests/pydantic_v1/resolver/test_3_tuple_list.py
@@ -8,12 +8,12 @@
 class Student(BaseModel):
     new_books: List[str] = []    
     async def resolve_new_books(self) -> List[str]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ['book1', 'book2']
 
     old_books: List[str] = []    
     async def resolve_old_books(self) -> List[str]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ['book1', 'book2']
 
 
diff --git a/tests/pydantic_v1/resolver/test_40_multiple_collect_source.py b/tests/pydantic_v1/resolver/test_40_multiple_collect_source.py
index 66b9a00..935f85b 100644
--- a/tests/pydantic_v1/resolver/test_40_multiple_collect_source.py
+++ b/tests/pydantic_v1/resolver/test_40_multiple_collect_source.py
@@ -1,8 +1,7 @@
 from __future__ import annotations
 from pydantic import BaseModel
 from typing import List
-from pydantic_resolve import Resolver, Collector
-from pydantic_resolve.core import LoaderDepend
+from pydantic_resolve import Resolver, Collector, LoaderDepend
 import pytest
 
 class Root(BaseModel):
diff --git a/tests/pydantic_v1/resolver/test_4_resolve_return_types.py b/tests/pydantic_v1/resolver/test_4_resolve_return_types.py
index f6b490e..fa04a06 100644
--- a/tests/pydantic_v1/resolver/test_4_resolve_return_types.py
+++ b/tests/pydantic_v1/resolver/test_4_resolve_return_types.py
@@ -6,7 +6,7 @@
 import pytest
 
 async def set_after(fut, value):
-    await asyncio.sleep(1)
+    await asyncio.sleep(.1)
     fut.set_result(value)
 
 class Student(BaseModel):
diff --git a/tests/pydantic_v1/resolver/test_6_resolve_dataclass.py b/tests/pydantic_v1/resolver/test_6_resolve_dataclass.py
index 260a61f..cb38763 100644
--- a/tests/pydantic_v1/resolver/test_6_resolve_dataclass.py
+++ b/tests/pydantic_v1/resolver/test_6_resolve_dataclass.py
@@ -14,7 +14,7 @@ class Car:
     wheels: List[Wheel] = field(default_factory=list)
 
     async def resolve_wheels(self) -> List[Wheel]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return [Wheel(is_ok=True)]
 
 @pytest.mark.asyncio
diff --git a/tests/pydantic_v1/utils/test_2_ensure_subset.py b/tests/pydantic_v1/utils/test_2_ensure_subset.py
index d0145cf..07f617f 100644
--- a/tests/pydantic_v1/utils/test_2_ensure_subset.py
+++ b/tests/pydantic_v1/utils/test_2_ensure_subset.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from pydantic import BaseModel
-from pydantic_resolve import util
+from pydantic_resolve.utils import class_util
 import pytest
 
 def test_ensure_subset():
@@ -13,11 +13,11 @@ class BaseX():
         a: str
         b: int
     
-    @util.ensure_subset(Base)
+    @class_util.ensure_subset(Base)
     class ChildA(BaseModel):
         a: str
 
-    @util.ensure_subset(Base)
+    @class_util.ensure_subset(Base)
     class ChildB(BaseModel):
         a: str
         c: int = 0
@@ -25,20 +25,20 @@ def resolve_c(self):
             return 21
 
     with pytest.raises(AttributeError):
-        @util.ensure_subset(Base)
+        @class_util.ensure_subset(Base)
         class ChildC(BaseModel):
             a: str
             b: str
 
     with pytest.raises(AttributeError):
-        @util.ensure_subset(Base)
+        @class_util.ensure_subset(Base)
         class ChildD(BaseModel):
             a: str
             b: int
             c: int
 
     with pytest.raises(AssertionError):
-        @util.ensure_subset(Base)
+        @class_util.ensure_subset(Base)
         @dataclass
         class ChildD():
             a: str
@@ -46,7 +46,7 @@ class ChildD():
             c: int
 
     with pytest.raises(AssertionError):
-        @util.ensure_subset(BaseX)
+        @class_util.ensure_subset(BaseX)
         @dataclass
         class ChildD():
             a: str
diff --git a/tests/pydantic_v1/utils/test_merge.py b/tests/pydantic_v1/utils/test_merge.py
index 8989d5f..7f6e5f4 100644
--- a/tests/pydantic_v1/utils/test_merge.py
+++ b/tests/pydantic_v1/utils/test_merge.py
@@ -1,5 +1,5 @@
 import pytest
-from pydantic_resolve.util import merge_dicts
+from pydantic_resolve.utils.params import merge_dicts
 from pydantic_resolve.exceptions import GlobalLoaderFieldOverlappedError
 
 
diff --git a/tests/pydantic_v1/utils/test_model_config.py b/tests/pydantic_v1/utils/test_model_config.py
index e9b8d59..1f78254 100644
--- a/tests/pydantic_v1/utils/test_model_config.py
+++ b/tests/pydantic_v1/utils/test_model_config.py
@@ -1,6 +1,5 @@
-from pydantic_resolve.util import model_config
 from pydantic import BaseModel, Field
-from pydantic_resolve import Resolver, LoaderDepend as LD, ensure_subset
+from pydantic_resolve import Resolver, LoaderDepend as LD, model_config, ensure_subset
 import pytest
 import json
 from typing import List
diff --git a/tests/pydantic_v1/utils/test_output.py b/tests/pydantic_v1/utils/test_output.py
deleted file mode 100644
index bbf845c..0000000
--- a/tests/pydantic_v1/utils/test_output.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from dataclasses import dataclass
-from pydantic_resolve.util import output
-from pydantic import BaseModel
-from typing import List, Optional
-import pytest
-
-def test_output():
-    @output
-    class A(BaseModel):
-        id: int
-        opt: Optional[str]
-
-        name: str = ''
-        def resolve_name(self):
-            return 'hi'
-
-        age: int = 0
-        def resolve_age(self):
-            return 1
-
-        greet: str = ''
-        def post_greet(self):
-            return 'hi'
-    
-    json_schema = A.schema()
-    assert set(json_schema['required']) == {'id', 'name', 'age', 'greet'}
-
-    a = A(id=1)
-    assert a.id == 1  # can assign
-
-
-def test_output2():
-
-    with pytest.raises(AttributeError):
-        @output
-        @dataclass
-        class A():
-            name: str
-            age: int
-
-
-def test_output_3():
-    
-    class B(BaseModel):
-        id: int 
-
-    class ABase(BaseModel):
-        id: int
-
-    @output
-    class A(ABase):
-        name: List[str] = [] 
-        def resolve_name(self):
-            return ['hi']
-
-        age: int = 0
-        def resolve_age(self):
-            return 1
-        
-        bs: List[B] = []
-        def resolve_bs(self):
-            return []
-    
-    json_schema = A.schema()
-    assert set(json_schema['required']) == {'id', 'name', 'age', 'bs'}
-
diff --git a/tests/pydantic_v1/utils/test_output_2.py b/tests/pydantic_v1/utils/test_output_2.py
deleted file mode 100644
index 6a058f6..0000000
--- a/tests/pydantic_v1/utils/test_output_2.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from pydantic_resolve.util import output
-from pydantic import BaseModel
-from typing import Optional
-
-def test_output():
-    """
-    output will not affect other class
-    """
-    @output
-    class X(BaseModel):
-        id: int
-        opt: Optional[str]
-
-    class A(BaseModel):
-        id: int
-        opt: Optional[str]
-
-        name: str = ''
-        def resolve_name(self):
-            return 'hi'
-
-        age: int = 0
-        def resolve_age(self):
-            return 1
-
-        greet: str = ''
-        def post_greet(self):
-            return 'hi'
-    
-    json_schema = A.schema()
-    assert set(json_schema['required']) == {'id'}
diff --git a/tests/pydantic_v1/utils/test_parse.py b/tests/pydantic_v1/utils/test_parse.py
index 7133e32..206007b 100644
--- a/tests/pydantic_v1/utils/test_parse.py
+++ b/tests/pydantic_v1/utils/test_parse.py
@@ -1,4 +1,4 @@
-from pydantic_resolve import util
+from pydantic_resolve.utils import conversion
 from typing import Optional
 from pydantic import BaseModel, ValidationError
 import pytest
@@ -9,14 +9,14 @@ class A(BaseModel):
         name: Optional[str]
     
     a = A(name=None)
-    value = util.try_parse_data_to_target_field_type(a, 'name', '123')
+    value = conversion.try_parse_data_to_target_field_type(a, 'name', '123')
     assert value == '123'
 
-    value = util.try_parse_data_to_target_field_type(a, 'name', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'name', None)
     assert value is None
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'name', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'name', [1,2,3])
 
 
 def test_dataclass():
@@ -29,14 +29,14 @@ class A:
         b: Optional[B] 
     
     a = A(b=None)
-    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', None)
     assert value is None 
 
-    value = util.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
     assert value == B(age=21)
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
 
 def test_mix():
     class B(BaseModel):
@@ -47,14 +47,14 @@ class A:
         b: Optional[B] 
     
     a = A(b=None)
-    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', None)
     assert value is None 
 
-    value = util.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
     assert value == B(age=21)
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
 
 def test_orm():
     class B(BaseModel):
@@ -72,11 +72,11 @@ def __init__(self, age: int):
 
     
     a = A(b=None)
-    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', None)
     assert value is None 
 
-    value = util.try_parse_data_to_target_field_type(a, 'b', BB(age=21))
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', BB(age=21))
     assert value == B(age=21)
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
diff --git a/tests/pydantic_v1/utils/test_utils.py b/tests/pydantic_v1/utils/test_utils.py
index e6e7ed0..59f4d76 100644
--- a/tests/pydantic_v1/utils/test_utils.py
+++ b/tests/pydantic_v1/utils/test_utils.py
@@ -1,10 +1,14 @@
 import asyncio
 from dataclasses import dataclass
 from typing import List
-from pydantic_resolve import util
+from pydantic_resolve.utils import experiment
 from pydantic import BaseModel
 import pytest
 
+import pydantic_resolve.utils.class_util
+import pydantic_resolve.utils.conversion
+import pydantic_resolve.utils.dataloader
+
 def test_get_class_field_annotations():
     class C:
         hello: str
@@ -18,9 +22,9 @@ class D(C):
     class E(C):
         world: str
     
-    assert list(util.get_class_field_annotations(C)) == ['hello']
-    assert list(util.get_class_field_annotations(D)) == []
-    assert list(util.get_class_field_annotations(E)) == ['world']
+    assert list(pydantic_resolve.utils.class_util.get_class_field_annotations(C)) == ['hello']
+    assert list(pydantic_resolve.utils.class_util.get_class_field_annotations(D)) == []
+    assert list(pydantic_resolve.utils.class_util.get_class_field_annotations(E)) == ['world']
 
 
 class User(BaseModel):
@@ -34,7 +38,7 @@ def test_build_object():
     users = [User(id=i[0], name=i[1], age=i[2]) for i in raw]
     a, b, c = users
     ids = [2, 3, 1, 4]
-    output = util.build_object(users, ids, lambda x: x.id)
+    output = pydantic_resolve.utils.dataloader.build_object(users, ids, lambda x: x.id)
     assert list(output) == [b, c, a, None]
     
 
@@ -43,7 +47,7 @@ def test_build_list():
     users = [User(id=i[0], name=i[1], age=i[2]) for i in raw]
     a, b, c = users
     ids = [2, 3, 1, 4]
-    output = util.build_list(users, ids, lambda x: x.id)
+    output = pydantic_resolve.utils.dataloader.build_list(users, ids, lambda x: x.id)
     assert list(output) == [[b], [c], [a], []]
 
 @pytest.mark.asyncio
@@ -63,7 +67,7 @@ async def kls_method(self, *args):
         v = await A.say(self, *args)
         return f'hello, {v}'
 
-    AA = util.replace_method(A, 'AA', 'say', kls_method)
+    AA = experiment.replace_method(A, 'AA', 'say', kls_method)
     k = AA('kimi')
     r2 = await k.say(['1', '2', '3'])
 
@@ -92,15 +96,15 @@ async def test_mapper_1():
     class A(BaseModel):
         a: int
 
-    @util.mapper(lambda x: A(**x))
+    @pydantic_resolve.utils.conversion.mapper(lambda x: A(**x))
     async def foo():
         return {'a': 1}
 
     async def call_later(f):
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         f.set_result({'a': 1})
 
-    @util.mapper(lambda x: A(**x))
+    @pydantic_resolve.utils.conversion.mapper(lambda x: A(**x))
     async def bar():
         lp = asyncio.get_event_loop()
         f = lp.create_future()
@@ -118,7 +122,7 @@ class A(BaseModel):
         a: int
     
     params = (A, {'a': 1})
-    ret = util._get_mapping_rule(*params)(*params)  # type:ignore
+    ret = pydantic_resolve.utils.conversion._get_mapping_rule(*params)(*params)  # type:ignore
     assert ret == A(a=1)
     
     @dataclass
@@ -126,7 +130,7 @@ class B:
         a: int
 
     params2 = (B, {'a': 1})
-    ret2 = util._get_mapping_rule(*params2)(*params2) # type:ignore
+    ret2 = pydantic_resolve.utils.conversion._get_mapping_rule(*params2)(*params2) # type:ignore
     assert ret2 == B(a=1)
 
 
@@ -141,12 +145,12 @@ def __init__(self, a):
             self.a = a
     
     p1 = (A, AA(1))
-    ret = util._get_mapping_rule(*p1)(*p1)  # type: ignore
+    ret = pydantic_resolve.utils.conversion._get_mapping_rule(*p1)(*p1)  # type: ignore
     assert ret == A(a=1)
 
     p2 = (A, {'a': 1})
     with pytest.raises(AttributeError):
-        util._get_mapping_rule(*p2)  # type: ignore
+        pydantic_resolve.utils.conversion._get_mapping_rule(*p2)  # type: ignore
     
 
 def test_auto_mapper_3():
@@ -156,10 +160,10 @@ class Config:
             orm_mode=True
     
     p1 = (A, A(a=1))
-    rule = util._get_mapping_rule(*p1)  # type: ignore
+    rule = pydantic_resolve.utils.conversion._get_mapping_rule(*p1)  # type: ignore
 
     assert rule is None
-    output = util._apply_rule(rule, *p1, is_list=False)
+    output = pydantic_resolve.utils.conversion._apply_rule(rule, *p1, is_list=False)
     assert output == A(a=1)
 
 
@@ -173,5 +177,5 @@ def __init__(self, a):
     
     p1 = (A, AA(a=1))
     with pytest.raises(AttributeError):
-        util._get_mapping_rule(*p1)(*p1)  # type: ignore
+        pydantic_resolve.utils.conversion._get_mapping_rule(*p1)(*p1)  # type: ignore
 
diff --git a/tests/pydantic_v2/core/test_field.py b/tests/pydantic_v2/core/test_field.py
index 4ccbd74..5f7b1c8 100644
--- a/tests/pydantic_v2/core/test_field.py
+++ b/tests/pydantic_v2/core/test_field.py
@@ -1,6 +1,6 @@
 # from __future__ import annotations
 from pydantic import BaseModel
-from pydantic_resolve.core import get_class
+from pydantic_resolve.utils.class_util import get_class
 
 def test_get_class():
     class Student(BaseModel):
diff --git a/tests/pydantic_v2/core/test_field_dataclass.py b/tests/pydantic_v2/core/test_field_dataclass.py
index adf4e59..17480f8 100644
--- a/tests/pydantic_v2/core/test_field_dataclass.py
+++ b/tests/pydantic_v2/core/test_field_dataclass.py
@@ -1,7 +1,7 @@
 # from __future__ import annotations
 from dataclasses import dataclass, field
 from typing import Optional, List
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 
 @dataclass
 class Queue:
@@ -44,7 +44,7 @@ def test_get_all_fields():
         'test_field_dataclass.Student': {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name'],
-            'attribute': ['zone', 'zeta2', 'zetas2'],
+            'object_fields': ['zone', 'zeta2', 'zetas2'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {},
             'has_context': False,
@@ -52,7 +52,7 @@ def test_get_all_fields():
         'test_field_dataclass.Zone': {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {},
             'has_context': False,
@@ -60,7 +60,7 @@ def test_get_all_fields():
         'test_field_dataclass.Queue': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'has_context': False,
@@ -68,7 +68,7 @@ def test_get_all_fields():
         'test_field_dataclass.Zeta': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'has_context': False,
diff --git a/tests/pydantic_v2/core/test_field_dataclass_anno.py b/tests/pydantic_v2/core/test_field_dataclass_anno.py
index 8a2dd98..6c0b87b 100644
--- a/tests/pydantic_v2/core/test_field_dataclass_anno.py
+++ b/tests/pydantic_v2/core/test_field_dataclass_anno.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass, field
 from typing import Optional, List
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 from pydantic_resolve import LoaderDepend, Collector
 
 
@@ -49,7 +49,7 @@ def test_get_all_fields():
         'test_field_dataclass_anno.Student': {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name', 'post_queue_names'],
-            'attribute': ['zone'],
+            'object_fields': ['zone'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {},
             'kls': Student
@@ -57,7 +57,7 @@ def test_get_all_fields():
         'test_field_dataclass_anno.Zone': {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zone
@@ -65,7 +65,7 @@ def test_get_all_fields():
         'test_field_dataclass_anno.Queue': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {'name': 'queue_name'},
             'kls': Queue
@@ -73,7 +73,7 @@ def test_get_all_fields():
         'test_field_dataclass_anno.Zeta': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zeta
diff --git a/tests/pydantic_v2/core/test_field_mix.py b/tests/pydantic_v2/core/test_field_mix.py
index 645a41b..62bbcf8 100644
--- a/tests/pydantic_v2/core/test_field_mix.py
+++ b/tests/pydantic_v2/core/test_field_mix.py
@@ -2,7 +2,7 @@
 from dataclasses import dataclass
 from typing import Optional, List
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 
 @dataclass
 class Book:
@@ -24,14 +24,14 @@ def test_get_all_fields():
         'test_field_mix.Student': {
             'resolve': ['resolve_books'],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {'name': 's_name'},
             'collect_dict': {}
         },
         'test_field_mix.Book': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
         },
diff --git a/tests/pydantic_v2/core/test_field_pydantic.py b/tests/pydantic_v2/core/test_field_pydantic.py
index 67c532b..7b2ff5b 100644
--- a/tests/pydantic_v2/core/test_field_pydantic.py
+++ b/tests/pydantic_v2/core/test_field_pydantic.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 from typing import Optional, List
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata, convert_metadata_key_as_kls
+from pydantic_resolve.analysis import scan_and_store_metadata, convert_metadata_key_as_kls
 from pydantic_resolve import LoaderDepend
 
 async def loader_fn(keys):
@@ -45,7 +45,7 @@ def test_get_all_fields():
         'test_field_pydantic.Student': {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name'],
-            'attribute': ['zones', 'zones2', 'zone'],
+            'object_fields': ['zones', 'zones2', 'zone'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {}
             # ... others
@@ -53,7 +53,7 @@ def test_get_all_fields():
         'test_field_pydantic.Zone': {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
@@ -61,7 +61,7 @@ def test_get_all_fields():
         'test_field_pydantic.Queue': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
@@ -69,7 +69,7 @@ def test_get_all_fields():
         'test_field_pydantic.Zeta': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
@@ -86,7 +86,7 @@ def test_convert_metadata():
         Student: {
             'resolve': ['resolve_name', 'resolve_zeta'],
             'post': ['post_name'],
-            'attribute': ['zones', 'zones2', 'zone'],
+            'object_fields': ['zones', 'zones2', 'zone'],
             'expose_dict': {'name': 'student_name'},
             'collect_dict': {},
             'kls': Student,
@@ -96,7 +96,7 @@ def test_convert_metadata():
         Zone: {
             'resolve': [],
             'post': [],
-            'attribute': ['qs'],
+            'object_fields': ['qs'],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zone,
@@ -106,7 +106,7 @@ def test_convert_metadata():
         Queue: {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Queue,
@@ -116,7 +116,7 @@ def test_convert_metadata():
         Zeta: {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {},
             'kls': Zeta,
diff --git a/tests/pydantic_v2/core/test_field_pydantic_error.py b/tests/pydantic_v2/core/test_field_pydantic_error.py
index 6d14eb9..07c34a1 100644
--- a/tests/pydantic_v2/core/test_field_pydantic_error.py
+++ b/tests/pydantic_v2/core/test_field_pydantic_error.py
@@ -2,7 +2,7 @@
 import pytest
 from typing import Optional
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata
+from pydantic_resolve.analysis import scan_and_store_metadata
 
 class A(BaseModel):
     __pydantic_resolve_expose__ = {'name': 'A_name'}
diff --git a/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py b/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py
index 969742f..70dcde6 100644
--- a/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py
+++ b/tests/pydantic_v2/core/test_field_validate_and_create_loader_instance.py
@@ -3,7 +3,7 @@
 from typing import Optional, List
 from pydantic import BaseModel
 from aiodataloader import DataLoader
-from pydantic_resolve.core import scan_and_store_metadata, validate_and_create_loader_instance
+from pydantic_resolve.analysis import scan_and_store_metadata, validate_and_create_loader_instance
 from pydantic_resolve import LoaderDepend, LoaderFieldNotProvidedError
 
 async def loader_fn(keys):
diff --git a/tests/pydantic_v2/core/test_scan_post_method.py b/tests/pydantic_v2/core/test_scan_post_method.py
index 9c8912f..21aa55d 100644
--- a/tests/pydantic_v2/core/test_scan_post_method.py
+++ b/tests/pydantic_v2/core/test_scan_post_method.py
@@ -1,5 +1,5 @@
 from pydantic import BaseModel
-from pydantic_resolve.core import _scan_post_method, _scan_post_default_handler
+from pydantic_resolve.analysis import _scan_post_method, _scan_post_default_handler
 from pydantic_resolve import Collector
 
 def test_scan_post_method_1():
@@ -39,17 +39,26 @@ def post_a(self, context, ancestor_context, parent):
 def test_scan_post_method_3():
     class A(BaseModel):
         a: str
-        def post_a(self, context, ancestor_context, collector=Collector(alias='c_name')):
+        def post_a(self,
+                   context,
+                   ancestor_context,
+                   collector=Collector(alias='c_name'),
+                   collector_2=Collector(alias='c_name'),
+                   ):
             return 2 * self.a
         
     result = _scan_post_method(A.post_a, 'post_a')
 
-    assert len(result['collectors']) == 1
+    assert len(result['collectors']) == 2
     assert result['collectors'][0]['field'] == 'post_a' 
     assert result['collectors'][0]['param'] == 'collector' 
     assert result['collectors'][0]['alias'] == 'c_name'
     assert isinstance(result['collectors'][0]['instance'], Collector)
 
+    assert result['collectors'][1]['field'] == 'post_a' 
+    assert result['collectors'][1]['param'] == 'collector_2' 
+    assert result['collectors'][1]['alias'] == 'c_name'
+    assert isinstance(result['collectors'][1]['instance'], Collector)
 
 def test_scan_post_method_4():
     class A(BaseModel):
diff --git a/tests/pydantic_v2/core/test_scan_resolve_method.py b/tests/pydantic_v2/core/test_scan_resolve_method.py
index c63ef43..4e180a6 100644
--- a/tests/pydantic_v2/core/test_scan_resolve_method.py
+++ b/tests/pydantic_v2/core/test_scan_resolve_method.py
@@ -1,7 +1,8 @@
 import pytest
 from aiodataloader import DataLoader
 from pydantic import BaseModel
-from pydantic_resolve.core import _scan_resolve_method, LoaderDepend, Collector
+from pydantic_resolve.analysis import _scan_resolve_method
+from pydantic_resolve import LoaderDepend, Collector
 
 def test_scan_resolve_method_1():
     class A(BaseModel):
diff --git a/tests/pydantic_v2/core/test_specific_type.py b/tests/pydantic_v2/core/test_specific_type.py
index 2ab8cf9..4db878f 100644
--- a/tests/pydantic_v2/core/test_specific_type.py
+++ b/tests/pydantic_v2/core/test_specific_type.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 from typing import Tuple
 from pydantic import BaseModel
-from pydantic_resolve.core import scan_and_store_metadata, convert_metadata_key_as_kls
+from pydantic_resolve.analysis import scan_and_store_metadata, convert_metadata_key_as_kls
 from pydantic_resolve import LoaderDepend
 
 async def loader_fn(keys):
@@ -17,7 +17,7 @@ def test_get_all_fields():
         'test_specific_type.Student': {
             'resolve': [],
             'post': [],
-            'attribute': [],
+            'object_fields': [],
             'expose_dict': {},
             'collect_dict': {}
             # ... others
diff --git a/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py b/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py
index 3117ced..d8dbc99 100644
--- a/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py
+++ b/tests/pydantic_v2/resolver/test_1_pydantic_resolve.py
@@ -13,7 +13,7 @@ def resolve_intro(self) -> str:
 
     books: List[str] = []    
     async def resolve_books(self) -> List[str]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ['book1', 'book2']
 
 
diff --git a/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py b/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py
index 4c24100..5c0dfd4 100644
--- a/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py
+++ b/tests/pydantic_v2/resolver/test_21_not_stop_by_idle_level.py
@@ -19,7 +19,7 @@ class B(BaseModel):
     name: str
     c: Optional[C] = None
     async def resolve_c(self) -> Optional[C]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return C(name='hello world')
 
 class A(BaseModel):
diff --git a/tests/pydantic_v2/resolver/test_27_context.py b/tests/pydantic_v2/resolver/test_27_context.py
index 9045e4c..ad00cf5 100644
--- a/tests/pydantic_v2/resolver/test_27_context.py
+++ b/tests/pydantic_v2/resolver/test_27_context.py
@@ -10,7 +10,7 @@ class Human(BaseModel):
     lucky: bool = True
     async def resolve_lucky(self):
         print('calculating...')
-        await asyncio.sleep(1)  # mock i/o
+        await asyncio.sleep(0.1)  # mock i/o
         return random() > 0.5
     
 class Earth(BaseModel):
diff --git a/tests/pydantic_v2/resolver/test_2_resolve_object.py b/tests/pydantic_v2/resolver/test_2_resolve_object.py
index aa80204..bd3ba7c 100644
--- a/tests/pydantic_v2/resolver/test_2_resolve_object.py
+++ b/tests/pydantic_v2/resolver/test_2_resolve_object.py
@@ -12,39 +12,39 @@ class ServiceDetail1(BaseModel):
     detail_a: Optional[DetailA] = None
 
     async def resolve_detail_a(self) -> Optional[DetailA]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return DetailA(name='hello world')
 
     detail_b: str = ''
 
     async def resolve_detail_b(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return 'good'
 
 class Service(BaseModel):
     service_detail_1: Optional[ServiceDetail1] = None
     async def resolve_service_detail_1(self) -> Optional[ServiceDetail1]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ServiceDetail1()
 
     service_detail_1b: Optional[ServiceDetail1] = None
     async def resolve_service_detail_1b(self) -> Optional[ServiceDetail1]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ServiceDetail1()
 
     service_detail_2: str = ''
     async def resolve_service_detail_2(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return "detail_2"
 
     service_detail_3: str = ''
     async def resolve_service_detail_3(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return "detail_3"
 
     service_detail_4: str = ''
     async def resolve_service_detail_4(self) -> str:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return "detail_4"
 
 @pytest.mark.asyncio
@@ -72,4 +72,4 @@ async def test_resolve_object():
     assert result.model_dump() == expected
     delta = time.time() - t
 
-    assert delta < 2.1
\ No newline at end of file
+    assert delta < 1
\ No newline at end of file
diff --git a/tests/pydantic_v2/resolver/test_35_collector.py b/tests/pydantic_v2/resolver/test_35_collector.py
index 4fbec79..b270d16 100644
--- a/tests/pydantic_v2/resolver/test_35_collector.py
+++ b/tests/pydantic_v2/resolver/test_35_collector.py
@@ -1,10 +1,9 @@
 from __future__ import annotations
 from pydantic import BaseModel
 from typing import List
-from pydantic_resolve import Resolver, Collector
+from pydantic_resolve import Resolver, Collector, LoaderDepend
 import pytest
 
-from pydantic_resolve.core import LoaderDepend
 
 class SubCollector(Collector):
     def add(self, val):  # replace with your implementation
diff --git a/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py b/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py
index 9878529..cc6017a 100644
--- a/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py
+++ b/tests/pydantic_v2/resolver/test_36_collector_level_by_level.py
@@ -1,8 +1,7 @@
 from __future__ import annotations
 from pydantic import BaseModel
 from typing import List
-from pydantic_resolve import Resolver, Collector
-from pydantic_resolve.core import LoaderDepend
+from pydantic_resolve import Resolver, Collector, LoaderDepend
 import pytest
 
 
diff --git a/tests/pydantic_v2/resolver/test_3_tuple_list.py b/tests/pydantic_v2/resolver/test_3_tuple_list.py
index 97dd371..647976d 100644
--- a/tests/pydantic_v2/resolver/test_3_tuple_list.py
+++ b/tests/pydantic_v2/resolver/test_3_tuple_list.py
@@ -8,12 +8,12 @@
 class Student(BaseModel):
     new_books: List[str] = []    
     async def resolve_new_books(self) -> List[str]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ['book1', 'book2']
 
     old_books: List[str] = []    
     async def resolve_old_books(self) -> List[str]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return ['book1', 'book2']
 
 
diff --git a/tests/pydantic_v2/resolver/test_40_post_async.py b/tests/pydantic_v2/resolver/test_40_post_async.py
index dce45b4..7d10064 100644
--- a/tests/pydantic_v2/resolver/test_40_post_async.py
+++ b/tests/pydantic_v2/resolver/test_40_post_async.py
@@ -9,7 +9,7 @@ class A(BaseModel):
 
     greet: str = ''
     async def post_greet(self):
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return 'hello ' + self.name
 
 @pytest.mark.asyncio
diff --git a/tests/pydantic_v2/resolver/test_4_resolve_return_types.py b/tests/pydantic_v2/resolver/test_4_resolve_return_types.py
index d56a76f..b7958c9 100644
--- a/tests/pydantic_v2/resolver/test_4_resolve_return_types.py
+++ b/tests/pydantic_v2/resolver/test_4_resolve_return_types.py
@@ -6,7 +6,7 @@
 import pytest
 
 async def set_after(fut, value):
-    await asyncio.sleep(1)
+    await asyncio.sleep(.1)
     fut.set_result(value)
 
 class Student(BaseModel):
diff --git a/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py b/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py
index 260a61f..cb38763 100644
--- a/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py
+++ b/tests/pydantic_v2/resolver/test_6_resolve_dataclass.py
@@ -14,7 +14,7 @@ class Car:
     wheels: List[Wheel] = field(default_factory=list)
 
     async def resolve_wheels(self) -> List[Wheel]:
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         return [Wheel(is_ok=True)]
 
 @pytest.mark.asyncio
diff --git a/tests/pydantic_v2/utils/test_2_ensure_subset.py b/tests/pydantic_v2/utils/test_2_ensure_subset.py
index 4f82b0f..3baeb32 100644
--- a/tests/pydantic_v2/utils/test_2_ensure_subset.py
+++ b/tests/pydantic_v2/utils/test_2_ensure_subset.py
@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from pydantic import BaseModel
-from pydantic_resolve import util
+from pydantic_resolve.utils import class_util
 import pytest
 
 def test_ensure_subset():
@@ -13,11 +13,11 @@ class BaseX():
         a: str
         b: int
     
-    @util.ensure_subset(Base)
+    @class_util.ensure_subset(Base)
     class ChildA(BaseModel):
         a: str
 
-    @util.ensure_subset(Base)
+    @class_util.ensure_subset(Base)
     class ChildB(BaseModel):
         a: str
         c: int = 0
@@ -25,20 +25,20 @@ def resolve_c(self):
             return 21
 
     with pytest.raises(AttributeError):
-        @util.ensure_subset(Base)
+        @class_util.ensure_subset(Base)
         class ChildC(BaseModel):
             a: str
             b: str
 
     with pytest.raises(AttributeError):
-        @util.ensure_subset(Base)
+        @class_util.ensure_subset(Base)
         class ChildD(BaseModel):
             a: str
             b: int
             c: int
 
     with pytest.raises(AssertionError):
-        @util.ensure_subset(Base)
+        @class_util.ensure_subset(Base)
         @dataclass
         class ChildE():
             a: str
@@ -46,7 +46,7 @@ class ChildE():
             c: int
 
     with pytest.raises(AssertionError):
-        @util.ensure_subset(BaseX)
+        @class_util.ensure_subset(BaseX)
         @dataclass
         class ChildF():
             a: str
diff --git a/tests/pydantic_v2/utils/test_model_config.py b/tests/pydantic_v2/utils/test_model_config.py
index 8c7f51e..0ec45de 100644
--- a/tests/pydantic_v2/utils/test_model_config.py
+++ b/tests/pydantic_v2/utils/test_model_config.py
@@ -1,4 +1,4 @@
-from pydantic_resolve.util import model_config
+from pydantic_resolve.utils.openapi import model_config
 from pydantic import BaseModel, Field
 from pydantic_resolve import Resolver
 import pytest
diff --git a/tests/pydantic_v2/utils/test_output.py b/tests/pydantic_v2/utils/test_output.py
deleted file mode 100644
index 7277ed3..0000000
--- a/tests/pydantic_v2/utils/test_output.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from dataclasses import dataclass
-from pydantic_resolve.util import output
-from pydantic import BaseModel
-from typing import List, Optional
-import pytest
-
-def test_output():
-    @output
-    class A(BaseModel):
-        id: int
-        opt: Optional[str] = None
-
-        name: str = ''
-        def resolve_name(self):
-            return 'hi'
-
-        age: int = 0
-        def resolve_age(self):
-            return 1
-
-        greet: str = ''
-        def post_greet(self):
-            return 'hi'
-    
-    json_schema = A.model_json_schema()
-    assert set(json_schema['required']) == {'id', 'name', 'age', 'greet'}
-
-    a = A(id=1)
-    assert a.id == 1  # can assign
-
-
-def test_output2():
-
-    with pytest.raises(AttributeError):
-        @output
-        @dataclass
-        class A():
-            name: str
-            age: int
-
-
-def test_output_3():
-    
-    class B(BaseModel):
-        id: int 
-
-    class ABase(BaseModel):
-        id: int
-
-    @output
-    class A(ABase):
-        name: List[str] = [] 
-        def resolve_name(self):
-            return ['hi']
-
-        age: int = 0
-        def resolve_age(self):
-            return 1
-        
-        bs: List[B] = []
-        def resolve_bs(self):
-            return []
-    
-    json_schema = A.model_json_schema()
-    assert set(json_schema['required']) == {'id', 'name', 'age', 'bs'}
-
diff --git a/tests/pydantic_v2/utils/test_output_2.py b/tests/pydantic_v2/utils/test_output_2.py
deleted file mode 100644
index c480a9c..0000000
--- a/tests/pydantic_v2/utils/test_output_2.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from dataclasses import dataclass
-from pydantic_resolve.util import output
-from pydantic import BaseModel
-from typing import List, Optional
-import pytest
-
-def test_output_will_not_affect_other():
-
-    @output
-    class X(BaseModel):
-        id: int
-        name: str
-
-    class A(BaseModel):
-        id: int
-        opt: Optional[str] = None
-
-        name: str = ''
-        def resolve_name(self):
-            return 'hi'
-
-        age: int = 0
-        def resolve_age(self):
-            return 1
-
-        greet: str = ''
-        def post_greet(self):
-            return 'hi'
-    
-    json_schema = A.model_json_schema()
-    assert set(json_schema['required']) == {'id'}
-
-    a = A(id=1)
-    assert a.id == 1  # can assign
-
diff --git a/tests/pydantic_v2/utils/test_parse.py b/tests/pydantic_v2/utils/test_parse.py
index 3750472..0f382e4 100644
--- a/tests/pydantic_v2/utils/test_parse.py
+++ b/tests/pydantic_v2/utils/test_parse.py
@@ -1,4 +1,4 @@
-from pydantic_resolve import util
+from pydantic_resolve.utils import conversion
 from typing import Optional
 from pydantic import ConfigDict, BaseModel, ValidationError
 import pytest
@@ -9,14 +9,14 @@ class A(BaseModel):
         name: Optional[str] = None
     
     a = A(name=None)
-    value = util.try_parse_data_to_target_field_type(a, 'name', '123')
+    value = conversion.try_parse_data_to_target_field_type(a, 'name', '123')
     assert value == '123'
 
-    value = util.try_parse_data_to_target_field_type(a, 'name', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'name', None)
     assert value is None
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'name', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'name', [1,2,3])
 
 
 def test_dataclass():
@@ -29,14 +29,14 @@ class A:
         b: Optional[B] 
     
     a = A(b=None)
-    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', None)
     assert value is None 
 
-    value = util.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
     assert value == B(age=21)
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
 
 def test_mix():
     class B(BaseModel):
@@ -47,14 +47,14 @@ class A:
         b: Optional[B] 
     
     a = A(b=None)
-    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', None)
     assert value is None 
 
-    value = util.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', {'age': 21})
     assert value == B(age=21)
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
 
 def test_orm():
     class B(BaseModel):
@@ -71,11 +71,11 @@ def __init__(self, age: int):
 
     
     a = A(b=None)
-    value = util.try_parse_data_to_target_field_type(a, 'b', None)
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', None)
     assert value is None 
 
-    value = util.try_parse_data_to_target_field_type(a, 'b', BB(age=21))
+    value = conversion.try_parse_data_to_target_field_type(a, 'b', BB(age=21))
     assert value == B(age=21)
 
     with pytest.raises(ValidationError):
-        util.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
+        conversion.try_parse_data_to_target_field_type(a, 'b', [1,2,3])
diff --git a/tests/pydantic_v2/utils/test_utils.py b/tests/pydantic_v2/utils/test_utils.py
index 67c2f24..5be4ca9 100644
--- a/tests/pydantic_v2/utils/test_utils.py
+++ b/tests/pydantic_v2/utils/test_utils.py
@@ -1,10 +1,14 @@
 import asyncio
 from dataclasses import dataclass
 from typing import List
-from pydantic_resolve import util
+from pydantic_resolve.utils import experiment
 from pydantic import ConfigDict, BaseModel
 import pytest
 
+import pydantic_resolve.utils.class_util
+import pydantic_resolve.utils.conversion
+import pydantic_resolve.utils.dataloader
+
 def test_get_class_field_annotations():
     class C:
         hello: str
@@ -18,9 +22,9 @@ class D(C):
     class E(C):
         world: str
     
-    assert list(util.get_class_field_annotations(C)) == ['hello']
-    assert list(util.get_class_field_annotations(D)) == []
-    assert list(util.get_class_field_annotations(E)) == ['world']
+    assert list(pydantic_resolve.utils.class_util.get_class_field_annotations(C)) == ['hello']
+    assert list(pydantic_resolve.utils.class_util.get_class_field_annotations(D)) == []
+    assert list(pydantic_resolve.utils.class_util.get_class_field_annotations(E)) == ['world']
 
 
 class User(BaseModel):
@@ -34,7 +38,7 @@ def test_build_object():
     users = [User(id=i[0], name=i[1], age=i[2]) for i in raw]
     a, b, c = users
     ids = [2, 3, 1, 4]
-    output = util.build_object(users, ids, lambda x: x.id)
+    output = pydantic_resolve.utils.dataloader.build_object(users, ids, lambda x: x.id)
     assert list(output) == [b, c, a, None]
     
 
@@ -43,7 +47,7 @@ def test_build_list():
     users = [User(id=i[0], name=i[1], age=i[2]) for i in raw]
     a, b, c = users
     ids = [2, 3, 1, 4]
-    output = util.build_list(users, ids, lambda x: x.id)
+    output = pydantic_resolve.utils.dataloader.build_list(users, ids, lambda x: x.id)
     assert list(output) == [[b], [c], [a], []]
 
 @pytest.mark.asyncio
@@ -63,7 +67,7 @@ async def kls_method(self, *args):
         v = await A.say(self, *args)
         return f'hello, {v}'
 
-    AA = util.replace_method(A, 'AA', 'say', kls_method)
+    AA = experiment.replace_method(A, 'AA', 'say', kls_method)
     k = AA('kimi')
     r2 = await k.say(['1', '2', '3'])
 
@@ -92,15 +96,15 @@ async def test_mapper_1():
     class A(BaseModel):
         a: int
 
-    @util.mapper(lambda x: A(**x))
+    @pydantic_resolve.utils.conversion.mapper(lambda x: A(**x))
     async def foo():
         return {'a': 1}
 
     async def call_later(f):
-        await asyncio.sleep(1)
+        await asyncio.sleep(.1)
         f.set_result({'a': 1})
 
-    @util.mapper(lambda x: A(**x))
+    @pydantic_resolve.utils.conversion.mapper(lambda x: A(**x))
     async def bar():
         lp = asyncio.get_event_loop()
         f = lp.create_future()
@@ -118,7 +122,7 @@ class A(BaseModel):
         a: int
     
     params = (A, {'a': 1})
-    ret = util._get_mapping_rule(*params)(*params)  # type:ignore
+    ret = pydantic_resolve.utils.conversion._get_mapping_rule(*params)(*params)  # type:ignore
     assert ret == A(a=1)
     
     @dataclass
@@ -126,7 +130,7 @@ class B:
         a: int
 
     params2 = (B, {'a': 1})
-    ret2 = util._get_mapping_rule(*params2)(*params2) # type:ignore
+    ret2 = pydantic_resolve.utils.conversion._get_mapping_rule(*params2)(*params2) # type:ignore
     assert ret2 == B(a=1)
 
 
@@ -140,12 +144,12 @@ def __init__(self, a):
             self.a = a
     
     p1 = (A, AA(1))
-    ret = util._get_mapping_rule(*p1)(*p1)  # type: ignore
+    ret = pydantic_resolve.utils.conversion._get_mapping_rule(*p1)(*p1)  # type: ignore
     assert ret == A(a=1)
 
     p2 = (A, {'a': 1})
     with pytest.raises(AttributeError):
-        util._get_mapping_rule(*p2)  # type: ignore
+        pydantic_resolve.utils.conversion._get_mapping_rule(*p2)  # type: ignore
     
 
 def test_auto_mapper_3():
@@ -154,10 +158,10 @@ class A(BaseModel):
         model_config = ConfigDict(from_attributes=True)
     
     p1 = (A, A(a=1))
-    rule = util._get_mapping_rule(*p1)  # type: ignore
+    rule = pydantic_resolve.utils.conversion._get_mapping_rule(*p1)  # type: ignore
 
     assert rule is None
-    output = util._apply_rule(rule, *p1, is_list=False)
+    output = pydantic_resolve.utils.conversion._apply_rule(rule, *p1, is_list=False)
     assert output == A(a=1)
 
 
@@ -171,5 +175,5 @@ def __init__(self, a):
     
     p1 = (A, AA(a=1))
     with pytest.raises(AttributeError):
-        util._get_mapping_rule(*p1)(*p1)  # type: ignore
+        pydantic_resolve.utils.conversion._get_mapping_rule(*p1)(*p1)  # type: ignore