Repository: python-scim/scim2-models

Issue:
Check that the `Schema` ids are URIs
[RFC7643 §7](https://datatracker.ietf.org/doc/html/rfc7643#section-7) indicates:

```
   id
      The unique URI of the schema.  When applicable, service providers
      MUST specify the URI, e.g.,
      "urn:ietf:params:scim:schemas:core:2.0:User".  Unlike most other
      schemas, which use some sort of Globally Unique Identifier (GUID)
      for the "id", the schema "id" is a URI so that it can be
      registered and is portable between different service providers and
      clients.  REQUIRED.
```

There should be a validator that checks that ids are URNs.

Discussion:

Solution patch:
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 541356f..f8d9d7e 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -12,6 +12,7 @@ Added
 Changed
 ^^^^^^^
 - :attr:`SearchRequest.attributes <scim2_models.SearchRequest.attributes>` and :attr:`SearchRequest.attributes <scim2_models.SearchRequest.excluded_attributes>` are mutually exclusive. #19
+- :class:`~scim2_models.Schema` ids must be valid URIs. #26
 
 [0.2.2] - 2024-09-20
 --------------------
diff --git a/scim2_models/rfc7643/schema.py b/scim2_models/rfc7643/schema.py
index 9d778c0..c1cae43 100644
--- a/scim2_models/rfc7643/schema.py
+++ b/scim2_models/rfc7643/schema.py
@@ -14,8 +14,10 @@
 from pydantic import Base64Bytes
 from pydantic import Field
 from pydantic import create_model
+from pydantic import field_validator
 from pydantic.alias_generators import to_pascal
 from pydantic.alias_generators import to_snake
+from pydantic_core import Url
 
 from ..base import CaseExact
 from ..base import ComplexAttribute
@@ -63,7 +65,10 @@ def make_python_model(
             for attr in (obj.attributes or [])
             if attr.name
         }
-        pydantic_attributes["schemas"] = (Optional[List[str]], Field(default=[obj.id]))
+        pydantic_attributes["schemas"] = (
+            Optional[List[str]],
+            Field(default=[obj.id]),
+        )
 
     model_name = to_pascal(to_snake(obj.name))
     model = create_model(model_name, __base__=base, **pydantic_attributes)
@@ -254,3 +259,10 @@ class Schema(Resource):
     ] = None
     """A complex type that defines service provider attributes and their
     qualities via the following set of sub-attributes."""
+
+    @field_validator("id")
+    @classmethod
+    def urn_id(cls, value: str) -> str:
+        """Schema ids are URI, as defined in RFC7643 §7."""
+
+        return str(Url(value))


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 76340a5..e7b7f4f 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,3 +1,6 @@
+import pytest
+from pydantic import ValidationError
+
 from scim2_models import Attribute
 from scim2_models import Mutability
 from scim2_models import Returned
@@ -72,3 +75,14 @@ def test_group_schema(load_sample):
     )
 
     assert obj.model_dump(exclude_unset=True) == payload
+
+
+def test_uri_ids():
+    """Test that schema ids are URI, as defined in RFC7643 §7.
+
+    https://datatracker.ietf.org/doc/html/rfc7643#section-7
+    """
+
+    Schema(id="urn:ietf:params:scim:schemas:extension:enterprise:2.0:User")
+    with pytest.raises(ValidationError):
+        Schema(id="invalid\nuri")