Repository: pydantic/pydantic

Issue:
Json Encoders are ignored in nested structures
### Checks

* [x] I added a descriptive title to this issue
* [x] I have searched (google, github) for similar issues and couldn't find anything
* [x] I have read and followed [the docs](https://pydantic-docs.helpmanual.io/) and still think this is a bug

<!-- Sorry to sound so draconian, but every second saved replying to issues is time spend improving pydantic :-) -->

# Bug

When nesting a model with customer encoders, they are ignored by the parent.

```python
from datetime import datetime, timedelta 
from pydantic import BaseModel 
from pydantic.json import timedelta_isoformat 
 
 
class WithCustomEncoders(BaseModel): 
    dt: datetime 
    diff: timedelta 
 
    class Config: 
        json_encoders = { 
            datetime: lambda v: v.timestamp(), 
            timedelta: timedelta_isoformat, 
        } 
 
class ParentWithoutEncoders(BaseModel): 
    child: WithCustomEncoders
 
m = WithCustomEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100)) 
print(m.json())

p = ParentWithoutEncoders(child=m)
print(p.json())


#> {"dt": 1969671600.0, "diff": "P4DT4H0M0.000000S"}
#> {"child": {"dt": "2032-06-01T00:00:00", "diff": 360000.0}}
```

Expected output:
```python
#> {"dt": 1969671600.0, "diff": "P4DT4H0M0.000000S"}
#> {"child": {"dt": 1969671600.0, "diff": "P4DT4H0M0.000000S"}}
```




Discussion:
Hello @mvanderlee 
The config depends on the model not the type of the field, even if this one is also a model.
So you'll need to duplicate your config or add it in your own custom BaseModel.
Duplicating the config is a non-starter for us. 
So you're saying that there is currently no way to ensure a Model will always be serialized a certain way? Other than manually defining a json encoder and always specifying it on json.dump. Which again, would be a non-started for us. 

I need to allow Models to be 2-way serializable.

You mentioned a custom BaseModel, do you have an example? 
Sure thing!
```python
from datetime import datetime, timedelta
from pydantic import BaseModel as PydanticBaseModel
from pydantic.json import timedelta_isoformat


class BaseModel(PydanticBaseModel):
    """
    All the instances of BaseModel should serialize
    those types the same way
    """
    class Config:
        json_encoders = {
            datetime: lambda v: v.timestamp(),
            timedelta: timedelta_isoformat,
        }


class WithCustomEncoders(BaseModel):
    dt: datetime
    diff: timedelta


class ParentWithoutEncoders(BaseModel):
    child: WithCustomEncoders


m = WithCustomEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
print(m.json())
# {"dt": 1969653600.0, "diff": "P4DT4H0M0.000000S"}

p = ParentWithoutEncoders(child=m)
print(p.json())
# {"child": {"dt": 1969653600.0, "diff": "P4DT4H0M0.000000S"}}
```
The magic is possible because `Config` of a subclass inherits from the `Config` of its parent!

You can also change default value of `BaseConfig`, which all `Config` classes inherit from

```python
from datetime import datetime, timedelta
from pydantic import BaseConfig, BaseModel
from pydantic.json import timedelta_isoformat

# All the instances of BaseModel should serialize those types the same way
BaseConfig.json_encoders = {
    datetime: lambda v: v.timestamp(),
    timedelta: timedelta_isoformat,
}


class WithCustomEncoders(BaseModel):
    dt: datetime
    diff: timedelta


class ParentWithoutEncoders(BaseModel):
    child: WithCustomEncoders


m = WithCustomEncoders(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
print(m.json())
# {"dt": 1969653600.0, "diff": "P4DT4H0M0.000000S"}

p = ParentWithoutEncoders(child=m)
print(p.json())
# {"child": {"dt": 1969653600.0, "diff": "P4DT4H0M0.000000S"}}
```

Hope it helps :)
Right, but now if I add a datetime field to the parent. Then both would use the same format. 
I'm looking to be able to define how a particular Model serializes, not all data types within the context. 

We can validate, and deserialize per model, but not serialize per model as far I can tell. 


```python
from datetime import datetime, timedelta
from pydantic import BaseModel, validator
from pydantic.json import timedelta_isoformat


class WithCustomEncoders(BaseModel):
    dt: datetime
    diff: timedelta

    @validator('dt', pre=True)
    def validate_dt(cls, v):
        try:
            return datetime.fromtimestamp(v)
        except Exception as e:
            raise ValueError('must be a valid timestamp', e)

    class Config:
        json_encoders = {
            datetime: lambda v: v.timestamp(),
            timedelta: timedelta_isoformat,
        }


class ParentWithoutEncoders(BaseModel):
    child: WithCustomEncoders
    p_dt: datetime

    @validator('p_dt', pre=True)
    def validate_p_dt(cls, v):
        try:
            return datetime.fromisoformat(v)
        except Exception as e:
            raise ValueError('must be valid iso string', e)

    class Config:
        json_encoders = {
            datetime: lambda v: v.isoformat()
        }

raw_m = '{"dt": 1969671600.0, "diff": "P4DT4H0M0.000000S"}'
raw_p = '{"child": {"dt": 1969671600.0, "diff": "P4DT4H0M0.000000S"}, "p_dt": "2032-06-01T00:00:00"}'

m = WithCustomEncoders.parse_raw(raw_m)
p = ParentWithoutEncoders.parse_raw(raw_p)

print(m.json())
print(p.json())

assert m.json() == raw_m
assert p.json() == raw_p

# {"dt": 1969671600.0, "diff": "P4DT4H0M0.000000S"}
# {"child": {"dt": "2032-06-01T00:00:00", "diff": 360000.0}, "p_dt": "2032-06-01T00:00:00"}

# Traceback (most recent call last):
#   File "<stdin>", line 50, in <module>
# AssertionError
```

The only way I currently see is to override the `_iter` function, i.e.:
```python
class WithCustomEncoders(BaseModel): 
    dt: datetime 
    diff: timedelta 
 
    @validator('dt', pre=True) 
    def validate_dt(cls, v): 
        try: 
            return datetime.fromtimestamp(v) 
        except Exception as e: 
            raise ValueError('must be a valid timestamp', e) 
 
    class Config: 
        json_encoders = { 
            datetime: lambda v: v.timestamp(), 
            timedelta: timedelta_isoformat, 
        } 
         
    def _iter(self, *args, **kwargs): 
        for key, v in super()._iter(*args, **kwargs): 
            if key == 'dt': 
                yield key, v.timestamp() 
            elif key == 'diff': 
                yield key, timedelta_isoformat(v) 
            else: 
                yield key, v
```

Edit: `_iter(...)` if it should also apply to `dict(...)` otherwise overriding `json(...)` in a similar fashion would work as well if it should only apply to json dumping.
I agree with @mvanderlee that it's not clear at first that the parent model that is converting to JSON establishes all the encoders and the nested models are converted using those. This appears to be because the whole object is converted into a dict and then the JSON encoder of the parent model is applied to the dictionary. An alternative might be to recursively call the JSON encoder on any pydantic BaseModel children and incorporate along the way.

@mvanderlee if you're interested in a PR, you might look at modifying https://github.com/samuelcolvin/pydantic/blob/13a5c7d676167b415080de5e6e6a74bea095b239/pydantic/main.py#L474-L509

This is likely to be much slower than the single call to JSON encoder, so I'd recommend making it an option or even a new method?
Agreed with the other folks here that I can see why the current behavior is implemented the way it is, but there may be use-cases where you might want to serialize different components of the same type differently. 

Perhaps I hadn't been looking at the right parts of the documentation, but I didn't really understand the `Config` inheritance until I stumbled upon this issue and saw [@PrettyWood's explanation](https://github.com/samuelcolvin/pydantic/issues/2277#issuecomment-764010272). Would it be possible to add some of this discussion to the documentation if it's not already there?

In the interim, based on @PrettyWood's example, what I came up with was just to create subclasses of the nested object (maybe this is overkill, would appreciate any suggestions!). Using a similar example:

```python
class DatetimeA(datetime.datetime):
    """Datetime that will be encoded as a string in pydantic Config."""
    def __new__(cls, *args, **kwargs):
        return datetime.datetime.__new__(cls, *args, **kwargs)

class DatetimeB(datetime.datetime):
    """Datetime that will be encoded as a timestamp in pydantic Config."""
    def __new__(cls, *args, **kwargs):
        return datetime.datetime.__new__(cls, *args, **kwargs)

class MyBaseModel(BaseModel):
    class Config:
        json_encoders = {
            DatetimeA: lambda v: str(v),
            DatetimeB: lambda v: v.timestamp()
    }

class TestModel(MyBaseModel):
    datetime_a: DatetimeA
    datetime_b: DatetimeB

class ParentModel(MyBaseModel):
    test_model: TestModel

test_model = TestModel(
    datetime_a=DatetimeA(2020, 1, 1),
    datetime_b=DatetimeB(2015, 1, 1)
)

parent_model = ParentModel(
    test_model=test_model
)

print(test_model.json())
print(parent_model.json())
```
Output:
```python
# '{"datetime_a": "2020-01-01 00:00:00", "datetime_b": 1420088400.0}'
# '{"test_model": {"datetime_a": "2020-01-01 00:00:00", "datetime_b": 1420088400.0}}'
```
I just ran into this issue... after a while of debugging I realized that my `Config` wasn't respected in nested models.

Although for my project it's OK to simply set a "global" encoder, I can see the value in defining how to encode a specific model within that model's Config and having that respected even when that model is nested. This is especially true when a module/library exposes public pydantic models for others to use.

Resolving patch:
diff --git a/docs/examples/exporting_models_json_nested_encoders.py b/docs/examples/exporting_models_json_nested_encoders.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/exporting_models_json_nested_encoders.py
@@ -0,0 +1,41 @@
+from datetime import datetime, timedelta
+from pydantic import BaseModel
+from pydantic.json import timedelta_isoformat
+
+
+class CustomChildModel(BaseModel):
+    dt: datetime
+    diff: timedelta
+
+    class Config:
+        json_encoders = {
+            datetime: lambda v: v.timestamp(),
+            timedelta: timedelta_isoformat,
+        }
+
+
+class ParentModel(BaseModel):
+    diff: timedelta
+    child: CustomChildModel
+
+    class Config:
+        json_encoders = {
+            timedelta: lambda v: v.total_seconds(),
+            CustomChildModel: lambda _: 'using parent encoder',
+        }
+
+
+child = CustomChildModel(dt=datetime(2032, 6, 1), diff=timedelta(hours=100))
+parent = ParentModel(diff=timedelta(hours=3), child=child)
+
+# default encoder uses total_seconds() for diff
+print(parent.json())
+
+# nested encoder uses isoformat
+print(parent.json(use_nested_encoders=True))
+
+# turning off models_as_dict only uses the top-level formatter, however
+
+print(parent.json(models_as_dict=False, use_nested_encoders=True))
+
+print(parent.json(models_as_dict=False, use_nested_encoders=False))
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -434,6 +434,7 @@ def dict(
         exclude_unset: bool = False,
         exclude_defaults: bool = False,
         exclude_none: bool = False,
+        encode_as_json: bool = False,
     ) -> 'DictStrAny':
         """
         Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
@@ -455,6 +456,7 @@ def dict(
                 exclude_unset=exclude_unset,
                 exclude_defaults=exclude_defaults,
                 exclude_none=exclude_none,
+                encode_as_json=encode_as_json,
             )
         )
 
@@ -470,6 +472,7 @@ def json(
         exclude_none: bool = False,
         encoder: Optional[Callable[[Any], Any]] = None,
         models_as_dict: bool = True,
+        use_nested_encoders: bool = False,
         **dumps_kwargs: Any,
     ) -> str:
         """
@@ -497,6 +500,7 @@ def json(
                 exclude_unset=exclude_unset,
                 exclude_defaults=exclude_defaults,
                 exclude_none=exclude_none,
+                encode_as_json=use_nested_encoders,
             )
         )
         if self.__custom_root_type__:
@@ -715,6 +719,7 @@ def _get_value(
         exclude_unset: bool,
         exclude_defaults: bool,
         exclude_none: bool,
+        encode_as_json: bool = False,
     ) -> Any:
 
         if isinstance(v, BaseModel):
@@ -726,6 +731,7 @@ def _get_value(
                     include=include,
                     exclude=exclude,
                     exclude_none=exclude_none,
+                    encode_as_json=encode_as_json,
                 )
                 if ROOT_KEY in v_dict:
                     return v_dict[ROOT_KEY]
@@ -775,6 +781,9 @@ def _get_value(
         elif isinstance(v, Enum) and getattr(cls.Config, 'use_enum_values', False):
             return v.value
 
+        elif encode_as_json:
+            return cls.__json_encoder__(v)
+
         else:
             return v
 
@@ -808,6 +817,7 @@ def _iter(
         exclude_unset: bool = False,
         exclude_defaults: bool = False,
         exclude_none: bool = False,
+        encode_as_json: bool = False,
     ) -> 'TupleGenerator':
 
         # Merge field set excludes with explicit exclude parameter with explicit overriding field set options.
@@ -853,6 +863,7 @@ def _iter(
                     exclude_unset=exclude_unset,
                     exclude_defaults=exclude_defaults,
                     exclude_none=exclude_none,
+                    encode_as_json=encode_as_json,
                 )
             yield dict_key, v
 


Test patch:
diff --git a/tests/test_json.py b/tests/test_json.py
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -372,3 +372,81 @@ class Model(BaseModel):
         nested: Optional[BaseModel]
 
     assert Model(value=None, nested=Model(value=None)).json(exclude_none=True) == '{"nested": {}}'
+
+
+class WithCustomEncoders(BaseModel):
+    dt: datetime.datetime
+    diff: datetime.timedelta
+
+    class Config:
+        json_encoders = {
+            datetime.datetime: lambda v: v.timestamp(),
+            datetime.timedelta: timedelta_isoformat,
+        }
+
+
+ides_of_march = datetime.datetime(44, 3, 15, tzinfo=datetime.timezone.utc)
+
+child = WithCustomEncoders(
+    dt=datetime.datetime(2032, 6, 1, tzinfo=datetime.timezone.utc),
+    diff=datetime.timedelta(hours=100),
+)
+
+
+def test_inner_custom_encoding():
+    assert child.json() == r'{"dt": 1969660800.0, "diff": "P4DT4H0M0.000000S"}'
+
+
+def test_encoding_in_parent_with_variable_encoders():
+    class ParentWithVariableEncoders(BaseModel):
+        dt: datetime.datetime
+        child: WithCustomEncoders
+
+        class Config:
+            json_encoders = {
+                datetime.datetime: lambda v: v.year,
+                datetime.timedelta: lambda v: v.total_seconds(),
+            }
+
+    parent = ParentWithVariableEncoders(child=child, dt=ides_of_march)
+
+    default = r'{"dt": 44, "child": {"dt": 2032, "diff": 360000.0}}'
+    assert parent.json() == default
+    # turning off models_as_dict defaults to top-level
+    assert parent.json(models_as_dict=False, use_nested_encoders=False) == default
+    assert parent.json(models_as_dict=False, use_nested_encoders=True) == default
+
+    custom = (
+        r'{"dt": 44, '  # parent.dt still uses the year to encode
+        # child uses child.json_encoders to encode
+        r'"child": {"dt": 1969660800.0, "diff": "P4DT4H0M0.000000S"}}'
+    )
+    assert parent.json(use_nested_encoders=True) == custom
+
+
+def test_encoding_in_parent_with_class_encoders():
+    class ParentWithClassEncoders(BaseModel):
+        dt: datetime.datetime
+        child: WithCustomEncoders
+
+        class Config:
+            json_encoders = {
+                datetime.datetime: lambda v: v.timestamp(),
+                WithCustomEncoders: lambda v: {'dt': v.dt.year},
+            }
+
+    parent = ParentWithClassEncoders(child=child, dt=ides_of_march)
+
+    # when models_as_dict=True, the `WithCustomEncoders` encoder is ignored
+    default = r'{"dt": -60772291200.0, "child": {"dt": 1969660800.0, "diff": 360000.0}}'
+    assert parent.json() == default
+
+    custom_child = r'{"dt": -60772291200.0, "child": {"dt": 1969660800.0, "diff": "P4DT4H0M0.000000S"}}'
+    assert parent.json(use_nested_encoders=True) == custom_child
+
+    # when models_as_dict=False, the parent `WithCustomEncoders` is used
+    # regardless of whatever json_encoders are in WithCustomEncoders.Config
+
+    custom_parent = r'{"dt": -60772291200.0, "child": {"dt": 2032}}'
+    assert parent.json(models_as_dict=False, use_nested_encoders=False) == custom_parent
+    assert parent.json(models_as_dict=False, use_nested_encoders=True) == custom_parent