Repository: pydantic/pydantic

Issue:
Supress title generation
# Feature Request

* OS: **Windows 10**
* Python version `import sys; print(sys.version)`: **3.7.1 (v3.7.1:260ec2c36a, Oct 20 2018, 14:57:15) [MSC v.1915 64 bit (AMD64)]**
* Pydantic version `import pydantic; print(pydantic.VERSION)`: **1.2**

There's no way to suppress/opt-out of the `title` property generation in `schema()`.
```py
# main.py
import pprint

import pydantic

class User(pydantic.BaseModel):
    name: str = pydantic.Field(..., title="", description="User name")
    password: str = pydantic.Field(..., description="User password")
    age: int = pydantic.Field(..., title=None, description="User age")


pprint.pprint(User.schema())
```
spits out:
```py
{'properties': {'age': {'description': 'User age',
                        'title': 'Age',
                        'type': 'integer'},
                'name': {'description': 'User name',
                         'title': 'Name',
                         'type': 'string'},
                'password': {'description': 'User password',
                             'title': 'Password',
                             'type': 'string'}},
 'required': ['name', 'password', 'age'],
 'title': 'User',
 'type': 'object'}
```
notice how all `properties` are given the auto-generated `title`.

I care about this mostly as a [fastapi](https://github.com/tiangolo/fastapi) user. I don't like how Swagger UI renders the title, and find the title to be redundant with `description` anyway (although maybe I'm missing something?)

This can be seen by adding the following to the code above:
```py
import fastapi

app = fastapi.FastAPI()

@app.post("/users")
def create_user(user: User):
    pass
```
and running the server with `uvicorn main:app` (note this will require `pip install fastapi uvicorn`).
`http://127.0.0.1:8000/docs` show:
![image](https://user-images.githubusercontent.com/6503002/69888068-dfce4400-12e1-11ea-8c38-ded37bf97f5f.png)

If I patch `pydantic/schema.py` with the following:
```py
    schema_overrides = False
    # s = dict(title=field.field_info.title or field.alias.title().replace('_', ' '))
    s = {}
    if field.field_info.title:
```
the output is much nicer (IMO):

![image](https://user-images.githubusercontent.com/6503002/69888268-d1ccf300-12e2-11ea-8315-b5cbcf630806.png)

I'd be happy to submit a PR. Adding a `Config` option probably makes sense, but maybe there should also be a way to disable the auto-title only for a specific field? 


Comments:
I don't think we want more config options unless absolutely required.

The solution here is to allow `schema_extra` to be a function which can mutate the schema, #889. Happy to accept a PR for that. 

Patch:
diff --git a/docs/build/exec_examples.py b/docs/build/exec_examples.py
--- a/docs/build/exec_examples.py
+++ b/docs/build/exec_examples.py
@@ -91,7 +91,6 @@ def exec_examples():
     errors = []
     all_md = all_md_contents()
     new_files = {}
-    os.environ.clear()
     os.environ.update({'my_auth_key': 'xxx', 'my_api_key': 'xxx'})
 
     sys.path.append(str(EXAMPLES_DIR))
@@ -113,7 +112,7 @@ def error(desc: str):
         if f'{{!.tmp_examples/{file.name}!}}' not in all_md:
             error('file not used anywhere')
 
-        file_text = file.read_text()
+        file_text = file.read_text('utf-8')
         if '\n\n\n' in file_text:
             error('too many new lines')
         if not file_text.endswith('\n'):
@@ -184,7 +183,7 @@ def error(desc: str):
     print(f'writing {len(new_files)} example files to {TMP_EXAMPLES_DIR}')
     TMP_EXAMPLES_DIR.mkdir()
     for file_name, content in new_files.items():
-        (TMP_EXAMPLES_DIR / file_name).write_text(content)
+        (TMP_EXAMPLES_DIR / file_name).write_text(content, 'utf-8')
     gen_ansi_output()
     return 0
 
diff --git a/docs/examples/schema_extra_callable.py b/docs/examples/schema_extra_callable.py
new file mode 100644
--- /dev/null
+++ b/docs/examples/schema_extra_callable.py
@@ -0,0 +1,15 @@
+# output-json
+from typing import Dict, Any
+from pydantic import BaseModel
+
+class Person(BaseModel):
+    name: str
+    age: int
+
+    class Config:
+        @staticmethod
+        def schema_extra(schema: Dict[str, Any]) -> None:
+            for prop in schema.get('properties', {}).values():
+                prop.pop('title', None)
+
+print(Person.schema_json(indent=2))
diff --git a/pydantic/main.py b/pydantic/main.py
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -66,7 +66,7 @@ class BaseConfig:
     getter_dict: Type[GetterDict] = GetterDict
     alias_generator: Optional[Callable[[str], str]] = None
     keep_untouched: Tuple[type, ...] = ()
-    schema_extra: Dict[str, Any] = {}
+    schema_extra: Union[Dict[str, Any], Callable[[Dict[str, Any]], None]] = {}
     json_loads: Callable[[str], Any] = json.loads
     json_dumps: Callable[..., str] = json.dumps
     json_encoders: Dict[AnyType, AnyCallable] = {}
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -466,7 +466,11 @@ def model_process_schema(
         model, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
     )
     s.update(m_schema)
-    s.update(model.__config__.schema_extra)
+    schema_extra = model.__config__.schema_extra
+    if callable(schema_extra):
+        schema_extra(s)
+    else:
+        s.update(schema_extra)
     return s, m_definitions, nested_models
 
 


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1482,6 +1482,22 @@ class Config:
     }
 
 
+def test_model_with_schema_extra_callable():
+    class Model(BaseModel):
+        name: str = None
+
+        class Config:
+            @staticmethod
+            def schema_extra(schema):
+                schema.pop('properties')
+                schema['type'] = 'override'
+
+    assert Model.schema() == {
+        'title': 'Model',
+        'type': 'override',
+    }
+
+
 def test_model_with_extra_forbidden():
     class Model(BaseModel):
         a: str