SCIM resources support two ways to access and modify attributes. The standard Python dot notation uses snake_case attribute names, while the bracket notation accepts SCIM paths as defined in :rfc:`RFC7644 §3.10 <7644#section-3.10>`.
>>> from scim2_models import User
>>> user = User(user_name="bjensen")
>>> user.display_name = "Barbara Jensen"
>>> user["nickName"] = "Babs"
>>> user["name.familyName"] = "Jensen"Attributes can be removed with del or by assigning :data:`None` to the attribute.
>>> del user["nickName"]
>>> user.nick_name is None
TrueUse Pydantic's :func:`~scim2_models.BaseModel.model_validate` method to parse and validate SCIM2 payloads.
>>> from scim2_models import User
>>> import datetime
>>> payload = {
... "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
... "id": "2819c223-7f76-453a-919d-413861904646",
... "userName": "bjensen@example.com",
... "meta": {
... "resourceType": "User",
... "created": "2010-01-23T04:56:22Z",
... "lastModified": "2011-05-13T04:42:34Z",
... "version": 'W\\/"3694e05e9dff590"',
... "location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
... },
... }
>>> user = User.model_validate(payload)
>>> user.user_name
'bjensen@example.com'
>>> user.meta.created # doctest: +ELLIPSIS
datetime.datetime(2010, 1, 23, 4, 56, 22, tzinfo=...)Pydantic :func:`~scim2_models.BaseModel.model_dump` method have been tuned to produce valid SCIM2 payloads.
>>> from scim2_models import User, Meta
>>> import datetime
>>> user = User(
... id="2819c223-7f76-453a-919d-413861904646",
... user_name="bjensen@example.com",
... meta=Meta(
... resource_type="User",
... created=datetime.datetime(2010, 1, 23, 4, 56, 22, tzinfo=datetime.timezone.utc),
... last_modified=datetime.datetime(2011, 5, 13, 4, 42, 34, tzinfo=datetime.timezone.utc),
... version='W\\/"3694e05e9dff590"',
... location="https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
... ),
... )
>>> dump = user.model_dump()
>>> assert dump == {
... "schemas": [
... "urn:ietf:params:scim:schemas:core:2.0:User"
... ],
... "id": "2819c223-7f76-453a-919d-413861904646",
... "meta": {
... "resourceType": "User",
... "created": "2010-01-23T04:56:22Z",
... "lastModified": "2011-05-13T04:42:34Z",
... "location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
... "version": "W\\/\"3694e05e9dff590\""
... },
... "userName": "bjensen@example.com"
... }The SCIM specifications detail some :class:`~scim2_models.Mutability` and :class:`~scim2_models.Returned` parameters for model attributes. Depending on the context, they will indicate that attributes should be present, absent, be ignored.
For instance, attributes marked as :attr:`~scim2_models.Mutability.read_only` should not be sent by SCIM clients on resource creation requests. By passing the right :class:`~scim2_models.Context` to the :meth:`~scim2_models.BaseModel.model_dump` method, only the expected fields will be dumped for this context:
>>> from scim2_models import User, Context
>>> user = User(user_name="bjensen@example.com")
>>> payload = user.model_dump(scim_ctx=Context.RESOURCE_CREATION_REQUEST)In the same fashion, by passing the right :class:`~scim2_models.Context` to the :meth:`~scim2_models.BaseModel.model_validate` method, fields with unexpected values will raise :class:`~pydantic.ValidationError`:
>>> from scim2_models import User, Context
>>> from pydantic import ValidationError
>>> try:
... obj = User.model_validate(payload, scim_ctx=Context.RESOURCE_CREATION_REQUEST)
... except pydantic.ValidationError:
... obj = Error(...):class:`~scim2_models.SCIMValidator` and :class:`~scim2_models.SCIMSerializer` are Pydantic Annotated markers that embed a :class:`~scim2_models.Context` directly in the type hint. They are useful for web framework integration where the framework handles parsing and serialization automatically.
:class:`~scim2_models.SCIMValidator` injects the context during validation:
>>> from typing import Annotated
>>> from pydantic import TypeAdapter
>>> from scim2_models import User, Context, SCIMValidator
>>> adapter = TypeAdapter(
... Annotated[User, SCIMValidator(Context.RESOURCE_CREATION_REQUEST)]
... )
>>> user = adapter.validate_python({
... "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
... "userName": "bjensen",
... "id": "should-be-stripped",
... })
>>> user.id is None
True:class:`~scim2_models.SCIMSerializer` injects the context during serialization:
>>> from scim2_models import SCIMSerializer
>>> adapter = TypeAdapter(
... Annotated[User, SCIMSerializer(Context.RESOURCE_QUERY_RESPONSE)]
... )
>>> user = User(user_name="bjensen", password="secret")
>>> user.id = "123"
>>> data = adapter.dump_python(user)
>>> "password" not in data
TrueThese annotations are pure Pydantic and carry no dependency on any web framework. In FastAPI for instance, they can be used directly in endpoint signatures:
@router.post("/Users", status_code=201)
async def create_user(
user: Annotated[User, SCIMValidator(Context.RESOURCE_CREATION_REQUEST)]
) -> Annotated[User, SCIMSerializer(Context.RESOURCE_CREATION_RESPONSE)]:
...See the :doc:`guides/fastapi` guide for a complete example.
In some situations it might be needed to exclude, or only include a given set of attributes when serializing a model.
This happens for instance when servers build response payloads for clients requesting only a sub-set the model attributes.
As defined in :rfc:`RFC7644 §3.9 <7644#section-3.9>`, attributes and excluded_attributes parameters can
be passed to :meth:`~scim2_models.BaseModel.model_dump`.
The expected attribute notation is the one detailed on :rfc:`RFC7644 §3.10 <7644#section-3.10>`,
like urn:ietf:params:scim:schemas:core:2.0:User:userName, or userName for short.
>>> from scim2_models import User, Context
>>> user = User(user_name="bjensen@example.com", display_name="bjensen")
>>> payload = user.model_dump(
... scim_ctx=Context.RESOURCE_QUERY_REQUEST,
... excluded_attributes=["displayName"]
... )
>>> assert payload == {
... "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
... "userName": "bjensen@example.com",
... "displayName": "bjensen",
... }Values read from :attr:`~scim2_models.SearchRequest.attributes` and :attr:`~scim2_models.SearchRequest.excluded_attributes` in :class:`~scim2_models.SearchRequest` objects can directly be used in :meth:`~scim2_models.BaseModel.model_dump`.
Attribute inclusions and exclusions interact with attributes :class:`~scim2_models.Returned`, in the server response :class:`Contexts <scim2_models.Context>`:
- attributes annotated with :attr:`~scim2_models.Returned.always` will always be dumped;
- attributes annotated with :attr:`~scim2_models.Returned.never` will never be dumped;
- attributes annotated with :attr:`~scim2_models.Returned.default` will be dumped unless being explicitly excluded;
- attributes annotated with :attr:`~scim2_models.Returned.request` will be not dumped unless being explicitly included.
:class:`~scim2_models.ListResponse` models take a type or a :data:`~typing.Union` of types.
You must pass the type you expect in the response, e.g. :class:`~scim2_models.ListResponse[User]` or :class:`~scim2_models.ListResponse[Union[User, Group]]`.
If a response resource type cannot be found, a pydantic.ValidationError will be raised.
>>> from typing import Union
>>> from scim2_models import User, Group, ListResponse
>>> payload = {
... "totalResults": 2,
... "itemsPerPage": 10,
... "startIndex": 1,
... "schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
... "Resources": [
... {
... "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
... "id": "2819c223-7f76-453a-919d-413861904646",
... "userName": "bjensen@example.com",
... "meta": {
... "resourceType": "User",
... "created": "2010-01-23T04:56:22Z",
... "lastModified": "2011-05-13T04:42:34Z",
... "version": 'W\\/"3694e05e9dff590"',
... "location": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
... },
... },
... {
... "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
... "id": "e9e30dba-f08f-4109-8486-d5c6a331660a",
... "displayName": "Tour Guides",
... "members": [
... {
... "value": "2819c223-7f76-453a-919d-413861904646",
... "$ref": "https://example.com/v2/Users/2819c223-7f76-453a-919d-413861904646",
... "display": "Babs Jensen",
... },
... {
... "value": "902c246b-6245-4190-8e05-00816be7344a",
... "$ref": "https://example.com/v2/Users/902c246b-6245-4190-8e05-00816be7344a",
... "display": "Mandy Pepperidge",
... },
... ],
... "meta": {
... "resourceType": "Group",
... "created": "2010-01-23T04:56:22Z",
... "lastModified": "2011-05-13T04:42:34Z",
... "version": 'W\\/"3694e05e9dff592"',
... "location": "https://example.com/v2/Groups/e9e30dba-f08f-4109-8486-d5c6a331660a",
... },
... },
... ],
... }
>>> response = ListResponse[Union[User, Group]].model_validate(payload)
>>> user, group = response.resources
>>> type(user)
<class 'scim2_models.resources.user.User'>
>>> type(group)
<class 'scim2_models.resources.group.Group'>:rfc:`RFC7643 §3.3 <7643#section-3.3>` extensions are supported.
Any class inheriting from :class:`~scim2_models.Extension` can be passed as a :class:`~scim2_models.Resource` type parameter, e.g. user = User[EnterpriseUser] or user = User[Union[EnterpriseUser, SuperHero]].
Extensions attributes are accessed with brackets, e.g. user[EnterpriseUser].employee_number, where user[EnterpriseUser] is a shortcut for user["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"].
>>> import datetime
>>> from scim2_models import User, EnterpriseUser, Meta
>>> user = User[EnterpriseUser](
... id="2819c223-7f76-453a-919d-413861904646",
... user_name="bjensen@example.com",
... meta=Meta(
... resource_type="User",
... created=datetime.datetime(
... 2010, 1, 23, 4, 56, 22, tzinfo=datetime.timezone.utc
... ),
... ),
... )
>>> user[EnterpriseUser] = EnterpriseUser(employee_number = "701984")
>>> user[EnterpriseUser].division="Theme Park"
>>> dump = user.model_dump()
>>> assert dump == {
... "schemas": [
... "urn:ietf:params:scim:schemas:core:2.0:User",
... "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
... ],
... "id": "2819c223-7f76-453a-919d-413861904646",
... "meta": {
... "resourceType": "User",
... "created": "2010-01-23T04:56:22Z"
... },
... "userName": "bjensen@example.com",
... "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
... "employeeNumber": "701984",
... "division": "Theme Park",
... }
... }scim2-models provides a hierarchy of exceptions corresponding to :rfc:`RFC7644 §3.12 <7644#section-3.12>` error types. Each exception can be converted to an :class:`~scim2_models.Error` response object or used in Pydantic validators.
Exceptions are named after their scimType value:
>>> from scim2_models import InvalidPathException, PathNotFoundException
>>> raise InvalidPathException(path="invalid..path")
Traceback (most recent call last):
...
scim2_models.exceptions.InvalidPathException: The path attribute was invalid or malformed
>>> raise PathNotFoundException(path="unknownAttr")
Traceback (most recent call last):
...
scim2_models.exceptions.PathNotFoundException: The specified path references a non-existent fieldUse :meth:`~scim2_models.SCIMException.to_error` to convert an exception to an :class:`~scim2_models.Error` response:
>>> from scim2_models import InvalidPathException
>>> exc = InvalidPathException(path="invalid..path")
>>> error = exc.to_error()
>>> error.status
400
>>> error.scim_type
'invalidPath'Use :meth:`Error.from_validation_error <scim2_models.Error.from_validation_error>` to convert a single Pydantic error to an :class:`~scim2_models.Error`:
>>> from pydantic import ValidationError
>>> from scim2_models import Error, User
>>> from scim2_models.base import Context
>>> try:
... User.model_validate({"userName": None}, context={"scim": Context.RESOURCE_CREATION_REQUEST})
... except ValidationError as exc:
... error = Error.from_validation_error(exc.errors()[0])
>>> error.scim_type
'invalidValue'Use :meth:`Error.from_validation_errors <scim2_models.Error.from_validation_errors>` to convert all errors at once:
>>> try:
... User.model_validate({"userName": 123, "displayName": 456})
... except ValidationError as exc:
... errors = Error.from_validation_errors(exc)
>>> len(errors)
2
>>> [e.detail for e in errors]
['Input should be a valid string: username', 'Input should be a valid string: displayname']The exhaustive list of exceptions is available in the :class:`reference <scim2_models.SCIMException>`.
You can write your own model and use it the same way than the other scim2-models models. Just inherit from :class:`~scim2_models.Resource` for your main resource, or :class:`~scim2_models.Extension` for extensions. Use :class:`~scim2_models.ComplexAttribute` as base class for complex attributes:
>>> from typing import Annotated, Optional
>>> from scim2_models import Resource, Returned, Mutability, ComplexAttribute, URN
>>> from enum import Enum
>>> class PetType(ComplexAttribute):
... type: Optional[str]
... """The pet type like 'cat' or 'dog'."""
...
... color: Optional[str]
... """The pet color."""
>>> class Pet(Resource):
... __schema__ = URN("urn:example:schemas:Pet")
...
... name: Annotated[Optional[str], Mutability.immutable, Returned.always]
... """The name of the pet."""
...
... pet_type: Optional[PetType]
... """The pet type."""You can annotate fields to indicate their :class:`~scim2_models.Mutability` and :class:`~scim2_models.Returned`. If unset the default values will be :attr:`~scim2_models.Mutability.read_write` and :attr:`~scim2_models.Returned.default`.
Warning
Be sure to make all the fields of your model :data:`~typing.Optional`. There will always be a :class:`~scim2_models.Context` in which this will be true.
There is a dedicated type for :rfc:`RFC7643 §2.3.7 <7643#section-2.3.7>` :class:`~scim2_models.Reference` that can take type parameters to represent :rfc:`RFC7643 §7 'referenceTypes'<7643#section-7>`:
>>> class PetOwner(Resource): ... pet: Reference["Pet"]
:class:`~scim2_models.Reference` has two special type parameters :data:`~scim2_models.External` and :data:`~scim2_models.URI` that matches :rfc:`RFC7643 §7 <7643#section-7>` external and URI reference types.
With :meth:`Resource.to_schema <scim2_models.Resource.to_schema>` and :meth:`Extension.to_schema <scim2_models.Extension.to_schema>`, any model can be exported as a :class:`~scim2_models.Schema` object.
This is useful for server implementations, so custom models or models provided by scim2-models can easily be exported on the /Schemas endpoint.
>>> from scim2_models import Resource, URN
>>> class MyCustomResource(Resource):
... """My awesome custom schema."""
...
... __schema__ = URN("urn:example:schemas:MyCustomResource")
...
... foobar: Optional[str]
...
>>> schema = MyCustomResource.to_schema()
>>> dump = schema.model_dump()
>>> assert dump == {
... "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Schema"],
... "id": "urn:example:schemas:MyCustomResource",
... "name": "MyCustomResource",
... "description": "My awesome custom schema.",
... "attributes": [
... {
... "caseExact": False,
... "multiValued": False,
... "mutability": "readWrite",
... "name": "foobar",
... "required": False,
... "returned": "default",
... "type": "string",
... "uniqueness": "none",
... },
... ],
... }Given a :class:`~scim2_models.Schema` object, scim2-models can dynamically generate a pythonic model to be used in your code with the :meth:`Resource.from_schema <scim2_models.Resource.from_schema>` and :meth:`Extension.from_schema <scim2_models.Extension.from_schema>` methods.
payload = {
"id": "urn:ietf:params:scim:schemas:core:2.0:Group",
"name": "Group",
"description": "Group",
"attributes": [
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "A human-readable name for the Group. REQUIRED.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
...
],
}
schema = Schema.model_validate(payload)
Group = Resource.from_schema(schema)
my_group = Group(display_name="This is my group")Client applications can use this to dynamically discover server resources by browsing the /Schemas endpoint.
Tip
Sub-Attribute models are automatically created and set as members of their parent model classes.
For instance the RFC7643 Group members sub-attribute can be accessed with Group.Members.
.. toggle::
.. literalinclude :: ../samples/rfc7643-8.7.1-schema-group.json
:language: json
:caption: schema-group.json
When handling a PUT request, validate the incoming payload with the
:attr:`~scim2_models.Context.RESOURCE_REPLACEMENT_REQUEST` context, then call
:meth:`~scim2_models.Resource.replace` against the existing resource to
verify that :attr:`~scim2_models.Mutability.immutable` attributes have not been
modified.
>>> from scim2_models import User, Context
>>> from scim2_models.exceptions import MutabilityException
>>> existing = User(user_name="bjensen")
>>> replacement = User.model_validate(
... {"userName": "bjensen"},
... scim_ctx=Context.RESOURCE_REPLACEMENT_REQUEST,
... )
>>> replacement.replace(existing)If an immutable attribute differs, a :class:`~scim2_models.MutabilityException` is raised.
:class:`~scim2_models.PatchOp` allows you to apply patch operations to modify SCIM resources. The :meth:`~scim2_models.PatchOp.patch` method applies operations in sequence and returns whether the resource was modified. The return code is a boolean indicating whether the object have been modified by the operations.
Note
:class:`~scim2_models.PatchOp` takes a type parameter that should be the class of the resource that is expected to be patched.
>>> from scim2_models import User, PatchOp, PatchOperation
>>> user = User(user_name="john.doe", nick_name="Johnny")
>>> payload = {
... "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
... "Operations": [
... {"op": "replace", "path": "nickName", "value": "John" },
... {"op": "add", "path": "emails", "value": [{"value": "john@example.com"}]},
... ]
... }
>>> patch = PatchOp[User].model_validate(
... payload, scim_ctx=Context.RESOURCE_PATCH_REQUEST
... )
>>> modified = patch.patch(user)
>>> print(modified)
True
>>> print(user.nick_name)
John
>>> print(user.emails[0].value)
john@example.comWarning
Patch operations are validated in the :attr:`~scim2_models.Context.RESOURCE_PATCH_REQUEST` context. Make sure to validate patch operations with the correct context to ensure proper validation of mutability and required constraints.
.. todo:: Bulk operations are not implemented yet, but any help is welcome!