Repository: sbrunner/jsonschema-gentypes

Issue:
Support for `patternProperties` when there is only one entry
I am working on a project where the JSON schema describes an object that has a single pattern property of type object. This property key is roughly the identifier for the sub-object. In this case the type structure is very similar to when `additionalProperties` is a dictionary. I've written a simple proof-of-concept and it appears to work nicely in the happy path and could provide a PR or sample code if interested.

Here is an example fragment of a JSON schema that has this pattern:
```json
    "providers": {
      "type": "object",
      "patternProperties": {
        "^[a-z0-9_]{2,64}$": {
          "type": "object",
          "properties": {
           ...
          }
        }
      },
```

Would you be willing to add support for this into your library?

Discussion:
@sbrunner: Relased in 2.4.0 :-)

Solution patch:
diff --git a/jsonschema_gentypes/__init__.py b/jsonschema_gentypes/__init__.py
index edee935..1e0e823 100644
--- a/jsonschema_gentypes/__init__.py
+++ b/jsonschema_gentypes/__init__.py
@@ -742,7 +742,6 @@ def get_description(
             "default",
             "examples",
             "contains",
-            "patternProperties",
             "dependencies",
             "propertyNames",
         ):
diff --git a/jsonschema_gentypes/api_draft_04.py b/jsonschema_gentypes/api_draft_04.py
index 48e1457..ac13896 100644
--- a/jsonschema_gentypes/api_draft_04.py
+++ b/jsonschema_gentypes/api_draft_04.py
@@ -109,6 +109,23 @@ class APIv4(API):
         elif isinstance(additional_properties, dict):
             sub_type = self.get_type(additional_properties, f"{proposed_name} additionalProperties")
             std_dict = CombinedType(NativeType("Dict"), [BuiltinType("str"), sub_type])
+        else:
+            pattern_properties = cast(
+                dict[
+                    str,
+                    Union[
+                        jsonschema_draft_04.JSONSchemaD4,
+                        jsonschema_draft_2020_12_applicator.JSONSchemaItemD2020,
+                    ],
+                ],
+                schema.get("patternProperties"),
+            )
+            if pattern_properties and len(pattern_properties) == 1:
+                schema.setdefault("used", set()).add("patternProperties")  # type: ignore[typeddict-item]
+                pattern_prop = list(pattern_properties.values())[0]
+                sub_type = self.get_type(pattern_prop, f"{proposed_name} Type")
+                std_dict = CombinedType(NativeType("Dict"), [BuiltinType("str"), sub_type])
+
         schema.setdefault("used", set()).add("properties")  # type: ignore[typeddict-item]
         properties = cast(
             dict[
diff --git a/pyproject.toml b/pyproject.toml
index 9279179..fc154ed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,7 @@ profile = "black"
 line_length = 110
 
 [tool.mypy]
-python_version = 3.9
+python_version = "3.9"
 warn_redundant_casts = true
 warn_unused_ignores = true
 ignore_missing_imports = true


Test patch:
diff --git a/tests/test_test.py b/tests/test_test.py
index ec98148..275e36b 100644
--- a/tests/test_test.py
+++ b/tests/test_test.py
@@ -360,6 +360,77 @@ TestBasicTypes = Dict[str, Any]
     )
 
 
+def test_pattern_properties_multiple():
+    type_ = get_types(
+        {
+            "type": "object",
+            "Title": "Pattern properties with tow patterns",
+            "patternProperties": {"^[a-z]+$": {"type": "string"}, "^[0-9]+$": {"type": "number"}},
+        }
+    )
+    assert (
+        "\n".join([d.rstrip() for d in type_.definition(None)])
+        == '''
+
+_Base = Dict[str, Any]
+""" Title: Pattern properties with tow patterns """
+'''
+    )
+
+
+def test_pattern_properties_string():
+    type_ = get_types(
+        {
+            "type": "object",
+            "title": "Pattern properties with one pattern as string",
+            "patternProperties": {
+                "^[a-z]+$": {"type": "string"},
+            },
+        }
+    )
+    assert (
+        "\n".join([d.rstrip() for d in type_.definition(None)])
+        == '''
+
+PatternPropertiesWithOnePatternAsString = Dict[str, str]
+""" Pattern properties with one pattern as string. """
+'''
+    )
+
+
+def test_pattern_properties_object():
+    type_ = get_types(
+        {
+            "type": "object",
+            "title": "Pattern properties with one pattern as object",
+            "patternProperties": {
+                "^[a-z]+$": {
+                    "type": "object",
+                    "properties": {
+                        "prop": {"type": "string"},
+                    },
+                },
+            },
+        }
+    )
+    assert (
+        "\n".join([d.rstrip() for d in type_.definition(None)])
+        == '''
+
+PatternPropertiesWithOnePatternAsObject = Dict[str, "_PatternPropertiesWithOnePatternAsObjectType"]
+""" Pattern properties with one pattern as object. """
+'''
+    )
+
+    assert (
+        "\n".join([d.rstrip() for d in type_.depends_on()[0].depends_on()[2].definition(None)])
+        == f"""
+
+class _PatternPropertiesWithOnePatternAsObjectType(TypedDict, total=False):
+    prop: str"""
+    )
+
+
 def test_boolean_const():
     type_ = get_types(
         {