@@ -413,7 +413,7 @@ def check_replacement_request_mutability(
413413 and original is not None
414414 ):
415415 try :
416- obj ._check_immutable_fields (original )
416+ obj ._apply_replace_constraints (original )
417417 except MutabilityException as exc :
418418 raise exc .as_pydantic_error () from exc
419419 return obj
@@ -461,30 +461,47 @@ def check_primary_attribute_uniqueness(self, info: ValidationInfo) -> Self:
461461
462462 return self
463463
464- def _check_immutable_fields (self , original : Self ) -> None :
465- """Check that immutable fields have not been modified compared to *original* .
464+ def _apply_replace_constraints (self , original : Self ) -> None :
465+ """Enforce RFC 7644 §3.5.1 replace (PUT) semantics .
466466
467- Recursively checks nested single-valued complex attributes.
467+ - ``readOnly`` fields are copied from *original* unconditionally.
468+ - ``immutable`` fields are copied from *original* when absent from
469+ ``self``; a :class:`~scim2_models.MutabilityException` is raised
470+ when the value differs.
471+
472+ Recursively applies to nested single-valued complex attributes.
468473 """
469474 from .attributes import is_complex_attribute
470475
471476 for field_name in type (self ).model_fields :
472477 mutability = type (self ).get_field_annotation (field_name , Mutability )
473- if mutability == Mutability .immutable and getattr (
474- original , field_name
475- ) != getattr (self , field_name ):
476- raise MutabilityException (attribute = field_name , mutability = "immutable" )
478+ original_val = getattr (original , field_name )
479+
480+ if mutability == Mutability .read_only :
481+ # RFC 7644 §3.5.1: "readOnly" values provided SHALL be ignored.
482+ setattr (self , field_name , original_val )
483+ elif mutability == Mutability .immutable :
484+ self_val = getattr (self , field_name )
485+ if self_val is None and original_val is not None :
486+ # RFC 7643 §7: "SHALL NOT be updated" — omitting an
487+ # immutable field is not a request to clear it.
488+ setattr (self , field_name , original_val )
489+ elif self_val != original_val :
490+ # RFC 7644 §3.5.1: input values MUST match.
491+ raise MutabilityException (
492+ attribute = field_name , mutability = "immutable"
493+ )
477494
478495 attr_type = type (self ).get_field_root_type (field_name )
479496 if (
480497 attr_type
481498 and is_complex_attribute (attr_type )
482499 and not type (self ).get_field_multiplicity (field_name )
483500 ):
484- original_val = getattr (original , field_name )
485- replacement_val = getattr (self , field_name )
486- if original_val is not None and replacement_val is not None :
487- replacement_val . _check_immutable_fields ( original_val )
501+ original_sub = getattr (original , field_name )
502+ replacement_sub = getattr (self , field_name )
503+ if original_sub is not None and replacement_sub is not None :
504+ replacement_sub . _apply_replace_constraints ( original_sub )
488505
489506 def _set_complex_attribute_urns (self ) -> None :
490507 """Navigate through attributes and sub-attributes of type ComplexAttribute, and mark them with a '_attribute_urn' attribute.
0 commit comments