Repository: pydantic/pydantic

Issue:
dataclass config fields not warranted in json schema (1.8)
### 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

# Bug

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.8
            pydantic compiled: True
                 install path: /Users/dafcok/miniconda3/lib/python3.6/site-packages/pydantic
               python version: 3.6.8 |Anaconda, Inc.| (default, Dec 29 2018, 19:04:46)  [GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]
                     platform: Darwin-19.6.0-x86_64-i386-64bit
     optional deps. installed: ['devtools', 'dotenv', 'email-validator', 'typing-extensions'
```
```py
from pydantic import BaseModel
from pydantic.dataclasses import dataclass

# works
class A(BaseModel):
    a: str

    class Config:
        fields = {"a": {"description": "descr"}}

assert A.schema()["properties"]["a"]["description"] == "descr"

# works on 1.7.3
# fails on 1.8
@dataclass(config=A.Config)
class Ad:
    a: str

assert Ad.__pydantic_model__.schema()["properties"]["a"]["description"] == "descr"
```
```bash
Traceback (most recent call last):
  File "tests/test_schema_descr.py", line 24, in <module>
    assert Ad.__pydantic_model__.schema()["properties"]["a"]["description"] == "descr"
KeyError: 'description
```
It seems that `config.fields` is not processed in the decorator.


Comments:
Thanks for reporting, see the discussion on #2437, this is rooted in a wider problem which I'm fixing now.

Patch:
diff --git a/pydantic/fields.py b/pydantic/fields.py
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -106,7 +106,8 @@ class FieldInfo(Representation):
         'extra',
     )
 
-    __field_constraints__ = {  # field constraints with the default value
+    # field constraints with the default value, it's also used in update_from_config below
+    __field_constraints__ = {
         'min_length': None,
         'max_length': None,
         'regex': None,
@@ -153,6 +154,20 @@ def get_constraints(self) -> Set[str]:
         """
         return {attr for attr, default in self.__field_constraints__.items() if getattr(self, attr) != default}
 
+    def update_from_config(self, from_config: Dict[str, Any]) -> None:
+        """
+        Update this FieldInfo based on a dict from get_field_info, only fields which have not been set are dated.
+        """
+        for attr_name, value in from_config.items():
+            try:
+                current_value = getattr(self, attr_name)
+            except AttributeError:
+                # attr_name is not an attribute of FieldInfo, it should therefore be added to extra
+                self.extra[attr_name] = value
+            else:
+                if current_value is self.__field_constraints__.get(attr_name, None):
+                    setattr(self, attr_name, value)
+
     def _validate(self) -> None:
         if self.default not in (Undefined, Ellipsis) and self.default_factory is not None:
             raise ValueError('cannot specify both default and default_factory')
@@ -354,17 +369,20 @@ def _get_field_info(
                 raise ValueError(f'cannot specify multiple `Annotated` `Field`s for {field_name!r}')
             field_info = next(iter(field_infos), None)
             if field_info is not None:
+                field_info.update_from_config(field_info_from_config)
                 if field_info.default not in (Undefined, Ellipsis):
                     raise ValueError(f'`Field` default cannot be set in `Annotated` for {field_name!r}')
                 if value not in (Undefined, Ellipsis):
                     field_info.default = value
+
         if isinstance(value, FieldInfo):
             if field_info is not None:
                 raise ValueError(f'cannot specify `Annotated` and value `Field`s together for {field_name!r}')
             field_info = value
-        if field_info is None:
+            field_info.update_from_config(field_info_from_config)
+        elif field_info is None:
             field_info = FieldInfo(value, **field_info_from_config)
-        field_info.alias = field_info.alias or field_info_from_config.get('alias')
+
         value = None if field_info.default_factory is not None else field_info.default
         field_info._validate()
         return field_info, value
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -142,6 +142,10 @@ class BaseConfig:
 
     @classmethod
     def get_field_info(cls, name: str) -> Dict[str, Any]:
+        """
+        Get properties of FieldInfo from the `fields` property of the config class.
+        """
+
         fields_value = cls.fields.get(name)
 
         if isinstance(fields_value, str):


Test patch:
diff --git a/tests/test_annotated.py b/tests/test_annotated.py
--- a/tests/test_annotated.py
+++ b/tests/test_annotated.py
@@ -2,12 +2,10 @@
 from typing import get_type_hints
 
 import pytest
+from typing_extensions import Annotated
 
 from pydantic import BaseModel, Field
 from pydantic.fields import Undefined
-from pydantic.typing import Annotated
-
-pytestmark = pytest.mark.skipif(not Annotated, reason='typing_extensions not installed')
 
 
 @pytest.mark.parametrize(
@@ -26,12 +24,12 @@
         ),
         # Test valid Annotated Field uses
         pytest.param(
-            lambda: Annotated[int, Field(description='Test')],
+            lambda: Annotated[int, Field(description='Test')],  # noqa: F821
             5,
             id='annotated-field-value-default',
         ),
         pytest.param(
-            lambda: Annotated[int, Field(default_factory=lambda: 5, description='Test')],
+            lambda: Annotated[int, Field(default_factory=lambda: 5, description='Test')],  # noqa: F821
             Undefined,
             id='annotated-field-default_factory',
         ),
@@ -132,3 +130,15 @@ class AnnotatedModel(BaseModel):
         one: Annotated[int, field]
 
     assert AnnotatedModel(one=1).dict() == {'one': 1}
+
+
+def test_config_field_info():
+    class Foo(BaseModel):
+        a: Annotated[int, Field(foobar='hello')]  # noqa: F821
+
+        class Config:
+            fields = {'a': {'description': 'descr'}}
+
+    assert Foo.schema(by_alias=True)['properties'] == {
+        'a': {'title': 'A', 'description': 'descr', 'foobar': 'hello', 'type': 'integer'},
+    }
diff --git a/tests/test_create_model.py b/tests/test_create_model.py
--- a/tests/test_create_model.py
+++ b/tests/test_create_model.py
@@ -1,6 +1,6 @@
 import pytest
 
-from pydantic import BaseModel, Extra, ValidationError, create_model, errors, validator
+from pydantic import BaseModel, Extra, Field, ValidationError, create_model, errors, validator
 
 
 def test_create_model():
@@ -194,3 +194,14 @@ class A(BaseModel):
 
     for field_name in ('x', 'y', 'z'):
         assert A.__fields__[field_name].default == DynamicA.__fields__[field_name].default
+
+
+def test_config_field_info_create_model():
+    class Config:
+        fields = {'a': {'description': 'descr'}}
+
+    m1 = create_model('M1', __config__=Config, a=(str, ...))
+    assert m1.schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
+
+    m2 = create_model('M2', __config__=Config, a=(str, Field(...)))
+    assert m2.schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -901,3 +901,22 @@ class Config:
     # ensure the restored dataclass is still a pydantic dataclass
     with pytest.raises(ValidationError, match='value\n +value is not a valid integer'):
         restored_obj.dataclass.value = 'value of a wrong type'
+
+
+def test_config_field_info_create_model():
+    # works
+    class A1(BaseModel):
+        a: str
+
+        class Config:
+            fields = {'a': {'description': 'descr'}}
+
+    assert A1.schema()['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
+
+    @pydantic.dataclasses.dataclass(config=A1.Config)
+    class A2:
+        a: str
+
+    assert A2.__pydantic_model__.schema()['properties'] == {
+        'a': {'title': 'A', 'description': 'descr', 'type': 'string'}
+    }
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
@@ -1779,3 +1779,63 @@ class MyModel(BaseModel):
         y: str = 'a'
 
     assert list(MyModel()._iter(by_alias=True)) == [('x', 1), ('y', 'a')]
+
+
+def test_config_field_info():
+    class Foo(BaseModel):
+        a: str = Field(...)
+
+        class Config:
+            fields = {'a': {'description': 'descr'}}
+
+    assert Foo.schema(by_alias=True)['properties'] == {'a': {'title': 'A', 'description': 'descr', 'type': 'string'}}
+
+
+def test_config_field_info_alias():
+    class Foo(BaseModel):
+        a: str = Field(...)
+
+        class Config:
+            fields = {'a': {'alias': 'b'}}
+
+    assert Foo.schema(by_alias=True)['properties'] == {'b': {'title': 'B', 'type': 'string'}}
+
+
+def test_config_field_info_merge():
+    class Foo(BaseModel):
+        a: str = Field(..., foo='Foo')
+
+        class Config:
+            fields = {'a': {'bar': 'Bar'}}
+
+    assert Foo.schema(by_alias=True)['properties'] == {
+        'a': {'bar': 'Bar', 'foo': 'Foo', 'title': 'A', 'type': 'string'}
+    }
+
+
+def test_config_field_info_allow_mutation():
+    class Foo(BaseModel):
+        a: str = Field(...)
+
+        class Config:
+            validate_assignment = True
+
+    assert Foo.__fields__['a'].field_info.allow_mutation is True
+
+    f = Foo(a='x')
+    f.a = 'y'
+    assert f.dict() == {'a': 'y'}
+
+    class Bar(BaseModel):
+        a: str = Field(...)
+
+        class Config:
+            fields = {'a': {'allow_mutation': False}}
+            validate_assignment = True
+
+    assert Bar.__fields__['a'].field_info.allow_mutation is False
+
+    b = Bar(a='x')
+    with pytest.raises(TypeError):
+        b.a = 'y'
+    assert b.dict() == {'a': 'x'}