Repository: pydantic/pydantic (Python)

Issue:
Improve auto-generated JSON Schema title when none provided
# Feature Request
## schema.py:280
Current:
```python
s = dict(title=schema.title or field.alias.title())
```
Proposed:
```python
# provide better title for PEP8 snake case names
s = dict(title=schema.title or field.alias.title().replace('_', ' '))
```


Discussion:
Would you accept this change as a PR?
PR sounds good.

I guess no harm in including this in v1 since it's so small.
Given this breaks a lot of fastapi tests (presumably mostly just requiring modifications to generated schemas), @tiangolo do you have any issues with this change?
Okay, let's wait until v1.1.
except I guess if it's a breaking change, it would make more sense to include it in v1.

@tiangolo what do you think?
@tiangolo would you like me to also update the title field in the schema unit tests to ensure that's all it is?
I'm pro this change, it seems like a no brainer, especially if it's a breaking change, to include it in v1.

@skewty please fix the PR and I'll merge.
#624 would essentially make loading and dumping camelCase names (JSON typical) easy but the change I made doesn't help when alias is camelCase.

example:  'thisFieldName' -> 'Thisfieldname'

I was a bit surprised that `alias` was used as a fallback when `title` is not specified instead of plain old `name`.

If `name` was used instead of `alias` it is likely reasonable to assume the name is in snake_case (PEP8 and all).  In this case, it capitalizes nicely.

Following the 80% / 20% rule..  I think 80% of the people would be using snake_case field names in python and camelCase names for aliases (JSON export).  Given this, I think using `name` instead of `alias` makes sense.  `title` is, of course, offered for those who have specific requirements.

As an alternative, we could try and detect snake_case using something simple like `if '_' in s` and applying a snakeToCamelCase converstion when false..   This of course is more compute intensive and "magic".

Your thoughts?
fine by me, but I'd be interested in @tiangolo's feedback.
Further to the small change suggested above.. I thought I would share my findings below and offer my labor to help with coding.  I appreciate all the great work, time and effort that has gone into pydantic and feel it will have a prominent place in the code I work on at work.  I am willing to assist and help with any suggestion / change I propose.  Additionally, I'd like to help get the project to v1.0 so **fastapi** can catch-up and then I can use it more at work.  (That is the deal I have with my Software Architect at work.)

------------------------------------------------

In looking for ways to achieve the title behavior I was looking for I investigated using sub-classing and monkey patching.  Neither would be easy / nice / maintainable.  I discovered that the (S.O.L.I.D.) **O**pen for (**some types of**) extension leaves some room for improvement as presently written.

During my brief investigation, I found that the biggest obstacle was that so many free standing methods are used (and are additionally called in a nested fashion).  A mix of composition, builder and inheritance could be leveraged here.

If these free standing methods were brought into BaseModel (or another class) as `@classmethod`s then functionality could be much more easily overridden.  (Neither the code, nor the unit tests would really need to change much either.)

Additionally, small code changes like adding a `_get_title() -> str` method could be added.

For Example:
```python
class FieldSchemaBuilder(...):
    ...
    @classmethod    
    def get_title(cls, field, schema) -> str:
        return schema.title or field.alias.title().replace('_', ' ')

    @classmethod
    def model_process_schema(...): ...

    @classmethod
    def model_type_schema(...): ...

    @classmethod
    def field_schema(...): 
        ...
        s = dict(title=cls._get_title(field, schema))
        ...

class BaseModel(...):
    ....
    __builder_class__ = FieldSchemaBuilder
    ....
```

As before, your thoughts?

The reasons we use free standing methods are:
1. They're often easier to reason with than complex class-based inheritance models
2. We want to avoid adding more attributes (methods, classmethods, properties etc.) to `BaseModel` since it pollutes the namespace and may conflict with fields people want to implement. Aliases are useful, but annoying and can be confusing.

The `__builder_class__` (or similar) would therefore be a good way of working around this. In turn I have two concerns with that approach:
1. It shouldn't negatively effect performance.
2. It should be as simple as possible to get your head around when you first meet pydantic.

I think a better approach might be to move these methods to the `Config` class, that way we don't need a new concept, and all the existing logic for inheritance on `Config` would work.

We might need to modify `Config` initialisation a little to provide access to the model, but that shouldn't be too hard.

@skewty what do you think?
@samuelcolvin I had considered that actually but feel that what those methods do isn't really "config" related.  When I asked myself what I felt they did "they build" came to mind.  With that I additionally didn't think it ideal design for `Config` to have two responsibilities. 

What if the builder methods were left where they are and more `get_title()` style methods were used by the builder methods as feature requests came in?  The `get_title()` methods could live inside `Config` as suggested.

NOTE: if `field.name` is used instead of `field.alias` to generate `schema.title` as proposed above I wouldn't need a get_title() at all.

What would the preferred approach be?:
1. create methods inside `BaseConfig` like `get_field_schema`.
2. create variables inside `BaseConfig` like `alias_generator`.

If the user will be doing the same boiler plate code for each `Config` then it makes sense to
allow them to specify a `BaseModel.__base_config_class__ = MyBaseConfig` if this isn't already possible (quick inspection leads me to believe it is hard coded).  I haven't put the time in to fully understand how your nested `Config` works in pydantic's code.  Perhaps I can find some time for this later today.

I could see in my work project (we have some interaction with web clients) using:

```python
class JSONBaseConfig(BaseConfig):
    alias_generator = snake_to_camel_case

class JSONBaseModel(BaseModel):
    __base_config_class__ = JSONBaseConfig
    def dict():
        # change default for by_alias=True
    def json():
        # change default for by_alias=True

class MyWorkClassA(JSONBaseModel):
    ...

class MyWorkClassB(JSONBaseModel):
    ...
    class Config:
        # some override not in MyWorkClassA
```
Let's fix `get_title` to not use alias and be implemented in the best way possible.

Further changes, eg. overriding lots of methods on config can wait for now and might not be required at all.
> Let's fix `get_title` to not use alias and be implemented in the best way possible.

Has this fix released?
>> Let's fix get_title to not use alias and be implemented in the best way possible.

> Has this fix released?

I think it wasn't :(



Patch:
diff --git a/changes/772-skewty.rst b/changes/772-skewty.rst
new file mode 100644
index 00000000000..cebcde1e23f
--- /dev/null
+++ b/changes/772-skewty.rst
@@ -0,0 +1,1 @@
+Improved auto-generated ``title`` field in JSON schema by converting underscore to space.
\ No newline at end of file
diff --git a/pydantic/schema.py b/pydantic/schema.py
index 7042ad124af..348e4a50d14 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -277,7 +277,7 @@ def field_schema(
     ref_prefix = ref_prefix or default_prefix
     schema_overrides = False
     schema = cast('Schema', field.schema)
-    s = dict(title=schema.title or field.alias.title())
+    s = dict(title=schema.title or field.alias.title().replace('_', ' '))
     if schema.title:
         schema_overrides = True
 


Test patch:
diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py
index 421325a6478..c528d7c5981 100644
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -381,7 +381,7 @@ class User:
         'properties': {
             'id': {'title': 'Id', 'type': 'integer'},
             'name': {'title': 'Name', 'default': 'John Doe', 'type': 'string'},
-            'signup_ts': {'title': 'Signup_Ts', 'type': 'string', 'format': 'date-time'},
+            'signup_ts': {'title': 'Signup Ts', 'type': 'string', 'format': 'date-time'},
         },
         'required': ['id'],
     }
diff --git a/tests/test_schema.py b/tests/test_schema.py
index a498187e0be..4755c3a603b 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -705,7 +705,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_address': {'title': 'Ip_Address', 'type': 'string', 'format': 'ipv4'}},
+        'properties': {'ip_address': {'title': 'Ip Address', 'type': 'string', 'format': 'ipv4'}},
         'required': ['ip_address'],
     }
 
@@ -718,7 +718,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_address': {'title': 'Ip_Address', 'type': 'string', 'format': 'ipv6'}},
+        'properties': {'ip_address': {'title': 'Ip Address', 'type': 'string', 'format': 'ipv6'}},
         'required': ['ip_address'],
     }
 
@@ -731,7 +731,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_address': {'title': 'Ip_Address', 'type': 'string', 'format': 'ipvanyaddress'}},
+        'properties': {'ip_address': {'title': 'Ip Address', 'type': 'string', 'format': 'ipvanyaddress'}},
         'required': ['ip_address'],
     }
 
@@ -744,7 +744,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_interface': {'title': 'Ip_Interface', 'type': 'string', 'format': 'ipv4interface'}},
+        'properties': {'ip_interface': {'title': 'Ip Interface', 'type': 'string', 'format': 'ipv4interface'}},
         'required': ['ip_interface'],
     }
 
@@ -757,7 +757,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_interface': {'title': 'Ip_Interface', 'type': 'string', 'format': 'ipv6interface'}},
+        'properties': {'ip_interface': {'title': 'Ip Interface', 'type': 'string', 'format': 'ipv6interface'}},
         'required': ['ip_interface'],
     }
 
@@ -770,7 +770,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_interface': {'title': 'Ip_Interface', 'type': 'string', 'format': 'ipvanyinterface'}},
+        'properties': {'ip_interface': {'title': 'Ip Interface', 'type': 'string', 'format': 'ipvanyinterface'}},
         'required': ['ip_interface'],
     }
 
@@ -783,7 +783,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_network': {'title': 'Ip_Network', 'type': 'string', 'format': 'ipv4network'}},
+        'properties': {'ip_network': {'title': 'Ip Network', 'type': 'string', 'format': 'ipv4network'}},
         'required': ['ip_network'],
     }
 
@@ -796,7 +796,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_network': {'title': 'Ip_Network', 'type': 'string', 'format': 'ipv6network'}},
+        'properties': {'ip_network': {'title': 'Ip Network', 'type': 'string', 'format': 'ipv6network'}},
         'required': ['ip_network'],
     }
 
@@ -809,7 +809,7 @@ class Model(BaseModel):
     assert model_schema == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'ip_network': {'title': 'Ip_Network', 'type': 'string', 'format': 'ipvanynetwork'}},
+        'properties': {'ip_network': {'title': 'Ip Network', 'type': 'string', 'format': 'ipvanynetwork'}},
         'required': ['ip_network'],
     }
 
@@ -1368,7 +1368,7 @@ class Model(BaseModel):
         'type': 'object',
         'properties': {
             'dep': {'$ref': '#/definitions/Dep'},
-            'dep_l': {'title': 'Dep_L', 'type': 'array', 'items': {'$ref': '#/definitions/Dep'}},
+            'dep_l': {'title': 'Dep L', 'type': 'array', 'items': {'$ref': '#/definitions/Dep'}},
         },
         'required': ['dep', 'dep_l'],
         'definitions': {