Repository: pydantic/pydantic

Issue:
Allow ConstrainedStr as K in Dict[K, V] types
<!-- Questions, Feature Requests, and Bug Reports are all welcome -->
<!-- delete as applicable: -->
# Feature Request

I would like to use `ConstrainedStr` types as keys in my mapping relationships. These should be represented as [patternProperties](https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.5.5) in the JSON schema.

```py
import pprint
import typing
import pydantic

EnvironmentVariable = pydantic.constr(regex=r'^([a-zA-Z_][a-zA-Z0-9_]*)$')
EnvironmentVariables = typing.Dict[EnvironmentVariable, str]


class Job(pydantic.BaseModel):
    env: EnvironmentVariables = {}


# expected output
pprint.pprint(Job.schema())
"""
{'title': 'Job',
 'type': 'object',
 'properties': {'env': {'title': 'Env',
   'default': {},
   'type': 'object',
   'patternProperties': {
     '^([a-zA-Z_][a-zA-Z0-9_]*)$': {'type': 'string'}}}}}
"""

# This already works
Job.parse_obj({"env": {"0123": "test"}}) 
"""
ValidationError: 1 validation error
env -> __key__
  string does not match regex "^([a-zA-Z_][a-zA-Z0-9_]*)$" (type=value_error.str.regex; pattern=^([a-zA-Z_][a-zA-Z0-9_]*)$)
"""
```



Discussion:
@tiangolo what do you think?

Resolving patch:
diff --git a/pydantic/schema.py b/pydantic/schema.py
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -376,7 +376,7 @@ def get_long_model_name(model: Type['main.BaseModel']):
     return f'{model.__module__}__{model.__name__}'.replace('.', '__')
 
 
-def field_type_schema(
+def field_type_schema(  # noqa: C901 (ignore complexity)
     field: Field,
     *,
     by_alias: bool,
@@ -405,16 +405,20 @@ def field_type_schema(
         definitions.update(f_definitions)
         return {'type': 'array', 'uniqueItems': True, 'items': f_schema}, definitions
     elif field.shape is Shape.MAPPING:
+        dict_schema = {'type': 'object'}
+        regex = getattr(field.key_field.type_, 'regex', None)
         f_schema, f_definitions = field_singleton_schema(
             field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix
         )
         definitions.update(f_definitions)
-        if f_schema:
-            # The dict values are not simply Any
-            return {'type': 'object', 'additionalProperties': f_schema}, definitions
-        else:
-            # The dict values are Any, no need to declare it
-            return {'type': 'object'}, definitions
+        if regex:
+            # Dict keys have a regex pattern
+            # f_schema might be a schema or empty dict, add it either way
+            dict_schema['patternProperties'] = {regex.pattern: f_schema}
+        elif f_schema:
+            # The dict values are not simply Any, so they need a schema
+            dict_schema['additionalProperties'] = f_schema
+        return dict_schema, definitions
     elif field.shape is Shape.TUPLE:
         sub_schema = []
         for sf in field.sub_fields:


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1019,3 +1019,20 @@ class Foo(BaseModel):
         'type': 'object',
         'properties': {'a': {'type': 'string', 'title': 'A', 'default': 'foo', 'examples': ['bar']}},
     }
+
+
+def test_schema_dict_constr():
+    regex_str = r'^([a-zA-Z_][a-zA-Z0-9_]*)$'
+    ConStrType = constr(regex=regex_str)
+    ConStrKeyDict = Dict[ConStrType, str]
+
+    class Foo(BaseModel):
+        a: ConStrKeyDict = {}
+
+    assert Foo.schema() == {
+        'title': 'Foo',
+        'type': 'object',
+        'properties': {
+            'a': {'type': 'object', 'title': 'A', 'default': {}, 'patternProperties': {regex_str: {'type': 'string'}}}
+        },
+    }