Repository: pydantic/pydantic

Issue:
dict(skip_defaults=True) doesn't apply to nested models

```python
from typing import List
from pydantic import BaseModel, Schema

DIGITAL_INPUT = 'digital_input'


class Payload(BaseModel):
    """Base class for all Combox payloads"""


class PointData(Payload):
    name: str = Schema(..., title='Name', min_length=1, max_length=32)
    gios_type: str = Schema(DIGITAL_INPUT, title='GIOS Type')


class PanelData(Payload):
    name: str = Schema(..., title='Name', min_length=1, max_length=32)
    points: List[PointData] = Schema(..., title='Points', min_length=1)


pd = PanelData.validate({'name': 'Dummy Panel', 'points': [{'name': 'Dummy Point', 'gios_type': DIGITAL_INPUT}]})
assert pd.dict(skip_defaults=True)['points'][0]['gios_type'] == DIGITAL_INPUT
print(pd.dict(skip_defaults=True))
```

```
{'name': 'Dummy Panel', 'points': [{'name': 'Dummy Point', 'gios_type': 'digital_input'}]}
```

Notice how the `dict` contains 'gios_type' field.


Discussion:
I agree that it should, PR welcome
@skewty in this case you are explicitly passing a value to `PanelData.validate` with `'gios_type': DIGITAL_INPUT`.

You are passing the same value as the one that would be used by default, but you are passing it explicitly.

If you don't pass an explicit value, and let the model take the default one (the same value, but using the default), then Pydantic respects the `skip_defaults`:

```Python
from typing import List
from pydantic import BaseModel, Schema

DIGITAL_INPUT = 'digital_input'


class Payload(BaseModel):
    """Base class for all Combox payloads"""


class PointData(Payload):
    name: str = Schema(..., title='Name', min_length=1, max_length=32)
    gios_type: str = Schema(DIGITAL_INPUT, title='GIOS Type')


class PanelData(Payload):
    name: str = Schema(..., title='Name', min_length=1, max_length=32)
    points: List[PointData] = Schema(..., title='Points', min_length=1)


pd = PanelData.validate({'name': 'Dummy Panel', 'points': [{'name': 'Dummy Point'}]})
assert 'gios_type' not in pd.dict(skip_defaults=True)['points'][0]
print(pd.dict(skip_defaults=True))
```

---

I consider this behavior (the current behavior) a feature (useful), for example in this example:

Let's say you have this model:

```Python
class Person(BaseModel):
    name: str
    pet: str = None
```

You have an item in your DB that contains:

```Python
{"name": "alice", "pet": "orion"}
```

Now let's say you receive an update to that item with:

```Python
{"name": "bob"}
```

As `pet` is not set, you can keep the current value of `"orion"`, but you can update the name to `"bob"`. Using `skip_defaults` skips the non-existing `"pet"` that would be set to `None` by default.

Now in your DB:

```Python
{"name": "bob", "pet": "orion"}
```

Then, if you receive an update to the same item with:

```Python
{"pet": None}
```

You could update your item in the DB, marking the pet as dead (`None`).

Now in your DB:

```Python
{"name": "bob", "pet": None}
```

In this case, Pydantic would be smart enough to know that `pet` was intentionally and explicitly set to `None`, even when that was the default value.

It's able to distinguish between values taken by default and values set, even if the value is the same.
@tiangolo thank you for taking the time to make such a complete reply and sorry for taking so long to get back to this.

> I consider this behavior (the current behavior) a feature (useful), for example in this example:
> ...
> In this case, Pydantic would be smart enough to know that `pet` was intentionally and explicitly set to `None`, even when that was the default value.
> 
> It's able to distinguish between values taken by default and values set, even if the value is the same.

To me anyway, it is surprising that " It's able to distinguish between values taken by default and values set, even if the value is the same."  To me, I see the feature more as a `skip_unset_defaults=True` rather than a `skip_defaults=True` because you are not really skipping default values as the parameter name suggests (to me).

That said, would an alternate parameter name be acceptable to accomplish my version be acceptable?  Or perhaps some sort of additional parameter like `strict=True` which I would set to `False` to accomplish what I was looking for when I created this issue?
I realize changing how `skip_defaults` parameter works would be a breaking change and such a change would be best done before v1.0.  So this issue / discussion is coming up at a good time :)
I'm not entirely clear what change/new feature you would like?
@samuelcolvin I think the request is to change the `skip_defaults` argument of `BaseModel.dict` to exclude values if they are equal to the default value (whether or not it was set), and to have a new keyword argument (called `skip_unset_defaults`; I would propose just `skip_unset`) that implements the existing functionality.

For what it's worth, I also found the `skip_defaults` behavior surprising at first; `skip_unset` might have been a clearer name, especially considering most other libraries wouldn't even be *capable* of supporting the `skip_defaults` feature, so I think most people wouldn't guess that's how it works. Separately, I feel like skipping defaults (whether or not they were set) is a relatively common feature request. (For example, it would make it a lot easier to skip nulls in many cases).

At this point I'm familiar and comfortable with the existing implementation, so I'm fine either way, but I can see some merits.
`skip_defaults` and `skip_unset` sounds reasonable to me.

One advantage is that there's no more logic required on parsing so performance (of raw parsing) would be unchanged.

My only concern is that `skip_defaults` has new behaviour but has a name that shadows a different old keyword argument which now has a new name. This will cause a lot of headaches when migrating to v1 since tests and setup won't fail immediately.

One solution would be to completely change the names to: `exclude_defaults` and `exclude_unset`. That way there can be no confusion with the names and it's more consistent with the `exclude` argument at the expensive for 3 more characters and one more syllable.

What do you think?
I like the consistency with `exclude`.
do we still want this before v1 is released?

If so it needs someone to start it in the next week or so.
I think it's worthwhile. I'll try to get to this this weekend if no one else wants to handle it sooner.
might require logic shared to #898 to get the default values.

Resolving patch:
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -231,6 +231,7 @@ def __new__(mcs, name, bases, namespace, **kwargs):  # noqa C901
         new_namespace = {
             '__config__': config,
             '__fields__': fields,
+            '__field_defaults__': {n: f.default for n, f in fields.items() if not f.required},
             '__validators__': vg.validators,
             '__pre_root_validators__': pre_root_validators + pre_rv_new,
             '__post_root_validators__': post_root_validators + post_rv_new,
@@ -252,6 +253,7 @@ class BaseModel(metaclass=ModelMetaclass):
     if TYPE_CHECKING:
         # populated by the metaclass, defined here to help IDEs only
         __fields__: Dict[str, ModelField] = {}
+        __field_defaults__: Dict[str, Any] = {}
         __validators__: Dict[str, AnyCallable] = {}
         __pre_root_validators__: List[AnyCallable]
         __post_root_validators__: List[AnyCallable]
@@ -303,15 +305,23 @@ def dict(
         include: Union['SetIntStr', 'DictIntStrAny'] = None,
         exclude: Union['SetIntStr', 'DictIntStrAny'] = None,
         by_alias: bool = False,
-        skip_defaults: bool = False,
+        skip_defaults: bool = None,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
     ) -> 'DictStrAny':
         """
         Generate a dictionary representation of the model, optionally specifying which fields to include or exclude.
         """
+        if skip_defaults is not None:
+            warnings.warn(
+                f'{self.__class__.__name__}.dict(): "skip_defaults" is deprecated and replaced by "exclude_unset"',
+                DeprecationWarning,
+            )
+            exclude_unset = skip_defaults
         get_key = self._get_key_factory(by_alias)
         get_key = partial(get_key, self.__fields__)
 
-        allowed_keys = self._calculate_keys(include=include, exclude=exclude, skip_defaults=skip_defaults)
+        allowed_keys = self._calculate_keys(include=include, exclude=exclude, exclude_unset=exclude_unset)
         return {
             get_key(k): v
             for k, v in self._iter(
@@ -320,7 +330,8 @@ def dict(
                 allowed_keys=allowed_keys,
                 include=include,
                 exclude=exclude,
-                skip_defaults=skip_defaults,
+                exclude_unset=exclude_unset,
+                exclude_defaults=exclude_defaults,
             )
         }
 
@@ -336,7 +347,9 @@ def json(
         include: Union['SetIntStr', 'DictIntStrAny'] = None,
         exclude: Union['SetIntStr', 'DictIntStrAny'] = None,
         by_alias: bool = False,
-        skip_defaults: bool = False,
+        skip_defaults: bool = None,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
         encoder: Optional[Callable[[Any], Any]] = None,
         **dumps_kwargs: Any,
     ) -> str:
@@ -345,8 +358,20 @@ def json(
 
         `encoder` is an optional function to supply as `default` to json.dumps(), other arguments as per `json.dumps()`.
         """
+        if skip_defaults is not None:
+            warnings.warn(
+                f'{self.__class__.__name__}.json(): "skip_defaults" is deprecated and replaced by "exclude_unset"',
+                DeprecationWarning,
+            )
+            exclude_unset = skip_defaults
         encoder = cast(Callable[[Any], Any], encoder or self.__json_encoder__)
-        data = self.dict(include=include, exclude=exclude, by_alias=by_alias, skip_defaults=skip_defaults)
+        data = self.dict(
+            include=include,
+            exclude=exclude,
+            by_alias=by_alias,
+            exclude_unset=exclude_unset,
+            exclude_defaults=exclude_defaults,
+        )
         if self.__custom_root_type__:
             data = data[ROOT_KEY]
         return self.__config__.json_dumps(data, default=encoder, **dumps_kwargs)
@@ -446,7 +471,7 @@ def copy(
             # skip constructing values if no arguments are passed
             v = self.__dict__
         else:
-            allowed_keys = self._calculate_keys(include=include, exclude=exclude, skip_defaults=False, update=update)
+            allowed_keys = self._calculate_keys(include=include, exclude=exclude, exclude_unset=False, update=update)
             if allowed_keys is None:
                 v = {**self.__dict__, **(update or {})}
             else:
@@ -457,7 +482,7 @@ def copy(
                             by_alias=False,
                             include=include,
                             exclude=exclude,
-                            skip_defaults=False,
+                            exclude_unset=False,
                             allowed_keys=allowed_keys,
                         )
                     ),
@@ -516,12 +541,19 @@ def _get_value(
         by_alias: bool,
         include: Optional[Union['SetIntStr', 'DictIntStrAny']],
         exclude: Optional[Union['SetIntStr', 'DictIntStrAny']],
-        skip_defaults: bool,
+        exclude_unset: bool,
+        exclude_defaults: bool,
     ) -> Any:
 
         if isinstance(v, BaseModel):
             if to_dict:
-                return v.dict(by_alias=by_alias, skip_defaults=skip_defaults, include=include, exclude=exclude)
+                return v.dict(
+                    by_alias=by_alias,
+                    exclude_unset=exclude_unset,
+                    exclude_defaults=exclude_defaults,
+                    include=include,
+                    exclude=exclude,
+                )
             else:
                 return v.copy(include=include, exclude=exclude)
 
@@ -534,7 +566,8 @@ def _get_value(
                     v_,
                     to_dict=to_dict,
                     by_alias=by_alias,
-                    skip_defaults=skip_defaults,
+                    exclude_unset=exclude_unset,
+                    exclude_defaults=exclude_defaults,
                     include=value_include and value_include.for_element(k_),
                     exclude=value_exclude and value_exclude.for_element(k_),
                 )
@@ -549,7 +582,8 @@ def _get_value(
                     v_,
                     to_dict=to_dict,
                     by_alias=by_alias,
-                    skip_defaults=skip_defaults,
+                    exclude_unset=exclude_unset,
+                    exclude_defaults=exclude_defaults,
                     include=value_include and value_include.for_element(i),
                     exclude=value_exclude and value_exclude.for_element(i),
                 )
@@ -584,12 +618,20 @@ def _iter(
         allowed_keys: Optional['SetStr'] = None,
         include: Union['SetIntStr', 'DictIntStrAny'] = None,
         exclude: Union['SetIntStr', 'DictIntStrAny'] = None,
-        skip_defaults: bool = False,
+        exclude_unset: bool = False,
+        exclude_defaults: bool = False,
     ) -> 'TupleGenerator':
 
         value_exclude = ValueItems(self, exclude) if exclude else None
         value_include = ValueItems(self, include) if include else None
 
+        if exclude_defaults:
+            if allowed_keys is None:
+                allowed_keys = set(self.__fields__)
+            for k, v in self.__field_defaults__.items():
+                if self.__dict__[k] == v:
+                    allowed_keys.discard(k)
+
         for k, v in self.__dict__.items():
             if allowed_keys is None or k in allowed_keys:
                 yield k, self._get_value(
@@ -598,20 +640,21 @@ def _iter(
                     by_alias=by_alias,
                     include=value_include and value_include.for_element(k),
                     exclude=value_exclude and value_exclude.for_element(k),
-                    skip_defaults=skip_defaults,
+                    exclude_unset=exclude_unset,
+                    exclude_defaults=exclude_defaults,
                 )
 
     def _calculate_keys(
         self,
         include: Optional[Union['SetIntStr', 'DictIntStrAny']],
         exclude: Optional[Union['SetIntStr', 'DictIntStrAny']],
-        skip_defaults: bool,
+        exclude_unset: bool,
         update: Optional['DictStrAny'] = None,
     ) -> Optional['SetStr']:
-        if include is None and exclude is None and skip_defaults is False:
+        if include is None and exclude is None and exclude_unset is False:
             return None
 
-        if skip_defaults:
+        if exclude_unset:
             keys = self.__fields_set__.copy()
         else:
             keys = set(self.__dict__.keys())


Test patch:
diff --git a/tests/test_construction.py b/tests/test_construction.py
--- a/tests/test_construction.py
+++ b/tests/test_construction.py
@@ -181,8 +181,8 @@ def test_copy_set_fields():
     m = ModelTwo(a=24, d=Model(a='12'))
     m2 = m.copy()
 
-    assert m.dict(skip_defaults=True) == {'a': 24.0, 'd': {'a': 12}}
-    assert m.dict(skip_defaults=True) == m2.dict(skip_defaults=True)
+    assert m.dict(exclude_unset=True) == {'a': 24.0, 'd': {'a': 12}}
+    assert m.dict(exclude_unset=True) == m2.dict(exclude_unset=True)
 
 
 def test_simple_pickle():
@@ -227,9 +227,9 @@ class Config:
 
 def test_pickle_fields_set():
     m = Model(a=24)
-    assert m.dict(skip_defaults=True) == {'a': 24}
+    assert m.dict(exclude_unset=True) == {'a': 24}
     m2 = pickle.loads(pickle.dumps(m))
-    assert m2.dict(skip_defaults=True) == {'a': 24}
+    assert m2.dict(exclude_unset=True) == {'a': 24}
 
 
 def test_copy_update_exclude():
@@ -246,5 +246,5 @@ class Model(BaseModel):
     assert m.copy(exclude={'c'}).dict() == {'d': {'a': 'ax', 'b': 'bx'}}
     assert m.copy(exclude={'c'}, update={'c': 42}).dict() == {'c': 42, 'd': {'a': 'ax', 'b': 'bx'}}
 
-    assert m._calculate_keys(exclude={'x'}, include=None, skip_defaults=False) == {'c', 'd'}
-    assert m._calculate_keys(exclude={'x'}, include=None, skip_defaults=False, update={'c': 42}) == {'d'}
+    assert m._calculate_keys(exclude={'x'}, include=None, exclude_unset=False) == {'c', 'd'}
+    assert m._calculate_keys(exclude={'x'}, include=None, exclude_unset=False, update={'c': 42}) == {'d'}
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -373,26 +373,70 @@ class Model(BaseModel):
     assert m.dict(include={'a', 'b'}, exclude={'a'}) == {'b': 2}
 
 
-def test_include_exclude_default():
+def test_include_exclude_unset():
     class Model(BaseModel):
         a: int
         b: int
         c: int = 3
         d: int = 4
+        e: int = 5
+        f: int = 6
 
-    m = Model(a=1, b=2)
-    assert m.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4}
-    assert m.__fields_set__ == {'a', 'b'}
-    assert m.dict(skip_defaults=True) == {'a': 1, 'b': 2}
+    m = Model(a=1, b=2, e=5, f=7)
+    assert m.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 7}
+    assert m.__fields_set__ == {'a', 'b', 'e', 'f'}
+    assert m.dict(exclude_unset=True) == {'a': 1, 'b': 2, 'e': 5, 'f': 7}
+
+    assert m.dict(include={'a'}, exclude_unset=True) == {'a': 1}
+    assert m.dict(include={'c'}, exclude_unset=True) == {}
+
+    assert m.dict(exclude={'a'}, exclude_unset=True) == {'b': 2, 'e': 5, 'f': 7}
+    assert m.dict(exclude={'c'}, exclude_unset=True) == {'a': 1, 'b': 2, 'e': 5, 'f': 7}
+
+    assert m.dict(include={'a', 'b', 'c'}, exclude={'b'}, exclude_unset=True) == {'a': 1}
+    assert m.dict(include={'a', 'b', 'c'}, exclude={'a', 'c'}, exclude_unset=True) == {'b': 2}
 
-    assert m.dict(include={'a'}, skip_defaults=True) == {'a': 1}
-    assert m.dict(include={'c'}, skip_defaults=True) == {}
 
-    assert m.dict(exclude={'a'}, skip_defaults=True) == {'b': 2}
-    assert m.dict(exclude={'c'}, skip_defaults=True) == {'a': 1, 'b': 2}
+def test_include_exclude_defaults():
+    class Model(BaseModel):
+        a: int
+        b: int
+        c: int = 3
+        d: int = 4
+        e: int = 5
+        f: int = 6
+
+    m = Model(a=1, b=2, e=5, f=7)
+    assert m.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 7}
+    assert m.__fields_set__ == {'a', 'b', 'e', 'f'}
+    assert m.dict(exclude_defaults=True) == {'a': 1, 'b': 2, 'f': 7}
+
+    assert m.dict(include={'a'}, exclude_defaults=True) == {'a': 1}
+    assert m.dict(include={'c'}, exclude_defaults=True) == {}
+
+    assert m.dict(exclude={'a'}, exclude_defaults=True) == {'b': 2, 'f': 7}
+    assert m.dict(exclude={'c'}, exclude_defaults=True) == {'a': 1, 'b': 2, 'f': 7}
+
+    assert m.dict(include={'a', 'b', 'c'}, exclude={'b'}, exclude_defaults=True) == {'a': 1}
+    assert m.dict(include={'a', 'b', 'c'}, exclude={'a', 'c'}, exclude_defaults=True) == {'b': 2}
+
+
+def test_skip_defaults_deprecated():
+    class Model(BaseModel):
+        x: int
+
+    m = Model(x=1)
+    match = r'Model.dict\(\): "skip_defaults" is deprecated and replaced by "exclude_unset"'
+    with pytest.warns(DeprecationWarning, match=match):
+        assert m.dict(skip_defaults=True)
+    with pytest.warns(DeprecationWarning, match=match):
+        assert m.dict(skip_defaults=False)
 
-    assert m.dict(include={'a', 'b', 'c'}, exclude={'b'}, skip_defaults=True) == {'a': 1}
-    assert m.dict(include={'a', 'b', 'c'}, exclude={'a', 'c'}, skip_defaults=True) == {'b': 2}
+    match = r'Model.json\(\): "skip_defaults" is deprecated and replaced by "exclude_unset"'
+    with pytest.warns(DeprecationWarning, match=match):
+        assert m.json(skip_defaults=True)
+    with pytest.warns(DeprecationWarning, match=match):
+        assert m.json(skip_defaults=False)
 
 
 def test_advanced_exclude():
@@ -474,12 +518,12 @@ class Config:
     m = Model(a=1, b=2)
     assert m.dict() == {'a': 1, 'b': 2, 'c': 3}
     assert m.__fields_set__ == {'a', 'b'}
-    assert m.dict(skip_defaults=True) == {'a': 1, 'b': 2}
+    assert m.dict(exclude_unset=True) == {'a': 1, 'b': 2}
 
     m2 = Model(a=1, b=2, d=4)
     assert m2.dict() == {'a': 1, 'b': 2, 'c': 3}
     assert m2.__fields_set__ == {'a', 'b'}
-    assert m2.dict(skip_defaults=True) == {'a': 1, 'b': 2}
+    assert m2.dict(exclude_unset=True) == {'a': 1, 'b': 2}
 
 
 def test_field_set_allow_extra():
@@ -494,12 +538,12 @@ class Config:
     m = Model(a=1, b=2)
     assert m.dict() == {'a': 1, 'b': 2, 'c': 3}
     assert m.__fields_set__ == {'a', 'b'}
-    assert m.dict(skip_defaults=True) == {'a': 1, 'b': 2}
+    assert m.dict(exclude_unset=True) == {'a': 1, 'b': 2}
 
     m2 = Model(a=1, b=2, d=4)
     assert m2.dict() == {'a': 1, 'b': 2, 'c': 3, 'd': 4}
     assert m2.__fields_set__ == {'a', 'b', 'd'}
-    assert m2.dict(skip_defaults=True) == {'a': 1, 'b': 2, 'd': 4}
+    assert m2.dict(exclude_unset=True) == {'a': 1, 'b': 2, 'd': 4}
 
 
 def test_field_set_field_name():
@@ -509,7 +553,7 @@ class Model(BaseModel):
         b: int = 3
 
     assert Model(a=1, field_set=2).dict() == {'a': 1, 'field_set': 2, 'b': 3}
-    assert Model(a=1, field_set=2).dict(skip_defaults=True) == {'a': 1, 'field_set': 2}
+    assert Model(a=1, field_set=2).dict(exclude_unset=True) == {'a': 1, 'field_set': 2}
     assert Model.construct(dict(a=1, field_set=3), {'a', 'field_set'}).dict() == {'a': 1, 'field_set': 3}
 
 
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -729,19 +729,19 @@ class MyModel(BaseModel):
     assert m.__fields_set__ == {'a', 'b'}
 
 
-def test_skip_defaults_dict():
+def test_exclude_unset_dict():
     class MyModel(BaseModel):
         a: int
         b: int = 2
 
     m = MyModel(a=5)
-    assert m.dict(skip_defaults=True) == {'a': 5}
+    assert m.dict(exclude_unset=True) == {'a': 5}
 
     m = MyModel(a=5, b=3)
-    assert m.dict(skip_defaults=True) == {'a': 5, 'b': 3}
+    assert m.dict(exclude_unset=True) == {'a': 5, 'b': 3}
 
 
-def test_skip_defaults_recursive():
+def test_exclude_unset_recursive():
     class ModelA(BaseModel):
         a: int
         b: int = 1
@@ -753,11 +753,11 @@ class ModelB(BaseModel):
 
     m = ModelB(c=5, e={'a': 0})
     assert m.dict() == {'c': 5, 'd': 2, 'e': {'a': 0, 'b': 1}}
-    assert m.dict(skip_defaults=True) == {'c': 5, 'e': {'a': 0}}
+    assert m.dict(exclude_unset=True) == {'c': 5, 'e': {'a': 0}}
     assert dict(m) == {'c': 5, 'd': 2, 'e': {'a': 0, 'b': 1}}
 
 
-def test_dict_skip_defaults_populated_by_alias():
+def test_dict_exclude_unset_populated_by_alias():
     class MyModel(BaseModel):
         a: str = Field('default', alias='alias_a')
         b: str = Field('default', alias='alias_b')
@@ -767,11 +767,11 @@ class Config:
 
     m = MyModel(alias_a='a')
 
-    assert m.dict(skip_defaults=True) == {'a': 'a'}
-    assert m.dict(skip_defaults=True, by_alias=True) == {'alias_a': 'a'}
+    assert m.dict(exclude_unset=True) == {'a': 'a'}
+    assert m.dict(exclude_unset=True, by_alias=True) == {'alias_a': 'a'}
 
 
-def test_dict_skip_defaults_populated_by_alias_with_extra():
+def test_dict_exclude_unset_populated_by_alias_with_extra():
     class MyModel(BaseModel):
         a: str = Field('default', alias='alias_a')
         b: str = Field('default', alias='alias_b')
@@ -781,8 +781,8 @@ class Config:
 
     m = MyModel(alias_a='a', c='c')
 
-    assert m.dict(skip_defaults=True) == {'a': 'a', 'c': 'c'}
-    assert m.dict(skip_defaults=True, by_alias=True) == {'alias_a': 'a', 'c': 'c'}
+    assert m.dict(exclude_unset=True) == {'a': 'a', 'c': 'c'}
+    assert m.dict(exclude_unset=True, by_alias=True) == {'alias_a': 'a', 'c': 'c'}
 
 
 def test_dir_fields():
diff --git a/tests/test_orm_mode.py b/tests/test_orm_mode.py
--- a/tests/test_orm_mode.py
+++ b/tests/test_orm_mode.py
@@ -122,7 +122,7 @@ class Config:
     model = Model.from_orm(foo)
     assert model.foo == 'Foo'
     assert model.bar == 1
-    assert model.dict(skip_defaults=True) == {'foo': 'Foo'}
+    assert model.dict(exclude_unset=True) == {'foo': 'Foo'}
     with pytest.raises(ValidationError):
         ModelInvalid.from_orm(foo)