Skip to content

BaseValueObject

Base class for value objects -- immutable domain elements without identity, defined entirely by their attributes. Two value objects with the same attributes are considered equal.

Key methods:

  • replace(**kwargs) -- Create a copy with selected fields changed (see Replacing Fields)
  • to_dict() -- Return field values as a dictionary
  • defaults() -- Override to set computed defaults during initialization

See Value Objects guide for practical usage and Value Objects concept for design rationale.

Bases: Element, BaseModel, OptionsMixin

Base class for value objects -- immutable domain elements without identity, defined entirely by their attributes.

Two value objects with the same attribute values are considered equal. Value objects are always embedded within aggregates or entities and cannot exist independently. They become immutable after construction -- any attempt to modify an attribute raises IncorrectUsageError.

Fields are declared using standard Python type annotations with optional Field constraints. Value objects cannot contain identifier or unique fields, and cannot have associations (HasOne, HasMany).

Supports @invariant.post decorators for validation rules that are checked after construction.

Source code in src/protean/core/value_object.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def __init__(self, *args: Any, **kwargs: Any) -> None:
    # Support template dict pattern: VO({"key": "val"}, key2="val2")
    # Keyword args take precedence over template dict values.
    if args:
        merged: dict[str, Any] = {}
        for template in args:
            if not isinstance(template, dict):
                raise AssertionError(
                    f"Positional argument {template} passed must be a dict. "
                    f"This argument serves as a template for loading common "
                    f"values.",
                )
            merged.update(template)
        merged.update(kwargs)
        kwargs = merged
    try:
        super().__init__(**kwargs)
    except PydanticValidationError as e:
        raise ValidationError(convert_pydantic_errors(e))

defaults

defaults() -> None

Placeholder for defaults. Override in subclass when an attribute's default depends on other attribute values.

Source code in src/protean/core/value_object.py
246
247
248
def defaults(self) -> None:
    """Placeholder for defaults. Override in subclass when
    an attribute's default depends on other attribute values."""

replace

replace(**kwargs: Any) -> Self

Return a new value object with specified fields replaced.

Similar to dataclasses.replace() — copies all current field values, overlays the provided kwargs, and constructs a new instance of the same class. Invariants are re-validated on the new instance.

Passing field=None explicitly sets the field to None (it does not keep the old value). Only omitted fields retain their original values.

Raises IncorrectUsageError if any key in kwargs is not a declared field.

Source code in src/protean/core/value_object.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
def replace(self, **kwargs: Any) -> Self:
    """Return a new value object with specified fields replaced.

    Similar to ``dataclasses.replace()`` — copies all current field values,
    overlays the provided *kwargs*, and constructs a new instance of the
    same class.  Invariants are re-validated on the new instance.

    Passing ``field=None`` explicitly sets the field to ``None`` (it does
    not keep the old value).  Only omitted fields retain their original
    values.

    Raises ``IncorrectUsageError`` if any key in *kwargs* is not a
    declared field.
    """
    fields = getattr(self, _FIELDS, {})
    unknown = set(kwargs) - set(fields)
    if unknown:
        raise IncorrectUsageError(
            f"Unknown field(s) for {type(self).__name__}: "
            + ", ".join(sorted(unknown))
        )

    new_data: dict[str, Any] = {}
    for fname in fields:
        new_data[fname] = kwargs.get(fname, getattr(self, fname))

    return type(self)(**new_data)

to_dict

to_dict() -> dict[str, Any]

Return data as a dictionary.

Source code in src/protean/core/value_object.py
302
303
304
305
306
307
def to_dict(self) -> dict[str, Any]:
    """Return data as a dictionary."""
    result: dict[str, Any] = {}
    for fname, shim in getattr(self, _FIELDS, {}).items():
        result[fname] = shim.as_dict(getattr(self, fname, None))
    return result

value_object_from_entity

Create a BaseValueObject subclass whose fields mirror entity_cls.

This eliminates the boilerplate of manually duplicating an entity's fields into a value object for use in commands and events.

PARAMETER DESCRIPTION
entity_cls

The entity (or aggregate) class to project.

TYPE: type[Any]

name

Override the generated class name (default: {EntityName}ValueObject).

TYPE: str | None DEFAULT: None

exclude

Field names to omit from the generated value object.

TYPE: set[str] | None DEFAULT: None

RETURNS DESCRIPTION
type[BaseValueObject]

A new BaseValueObject subclass.

Source code in src/protean/core/value_object.py
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
def value_object_from_entity(
    entity_cls: type[Any],
    name: str | None = None,
    exclude: set[str] | None = None,
) -> type[BaseValueObject]:
    """Create a ``BaseValueObject`` subclass whose fields mirror *entity_cls*.

    This eliminates the boilerplate of manually duplicating an entity's fields
    into a value object for use in commands and events.

    Args:
        entity_cls: The entity (or aggregate) class to project.
        name: Override the generated class name (default: ``{EntityName}ValueObject``).
        exclude: Field names to omit from the generated value object.

    Returns:
        A new ``BaseValueObject`` subclass.
    """
    exclude = exclude or set()
    vo_name = name or f"{entity_cls.__name__}ValueObject"

    annotations: dict[str, Any] = {}
    namespace: dict[str, Any] = {}
    # ValueObjectList is lazily imported above; use Any in the annotation
    # to avoid referencing it at module-import time.
    association_descriptors: dict[str, Any] = {}
    model_field_info = getattr(entity_cls, "model_fields", {})

    for key, value in get_fields(entity_cls).items():
        if key in exclude:
            continue
        if isinstance(value, Reference):
            continue
        if key.startswith("_"):
            continue

        if isinstance(value, HasOne):
            # Recursively convert associated entity to VO. ``to_cls`` is
            # ``str | type`` (lazy string ref or resolved class), but by the
            # time an entity is projected into a VO the target is a resolved
            # class.
            child_vo_cls = value_object_from_entity(cast("type[Any]", value.to_cls))
            vo_descriptor = ValueObjectField(value_object_cls=child_vo_cls)
            annotations[key] = Optional[child_vo_cls]
            namespace[key] = None
            association_descriptors[key] = vo_descriptor

        elif isinstance(value, HasMany):
            # Recursively convert to list of VOs. See ``to_cls`` narrowing note
            # above.
            child_vo_cls = value_object_from_entity(cast("type[Any]", value.to_cls))
            vo_descriptor = ValueObjectField(value_object_cls=child_vo_cls)
            # ``ValueObjectList.__init__`` has an untyped ``content_type``
            # parameter that defaults to ``str``, so pyright infers ``type[str]``.
            # A ValueObject descriptor is a documented, runtime-accepted value
            # for lists of value objects; the constructor validates it explicitly.
            list_descriptor = ValueObjectList(content_type=vo_descriptor)  # pyright: ignore[reportArgumentType]
            # ``child_vo_cls`` is a runtime-built VO class; mypy cannot model a
            # generic subscripted by a local variable (treats it as a type-alias
            # attempt). The runtime construction is valid.
            annotations[key] = list[child_vo_cls]  # type: ignore[valid-type]
            namespace[key] = PydanticField(default_factory=list)
            association_descriptors[key] = list_descriptor

        elif isinstance(value, ValueObjectField):
            vo_cls = value.value_object_cls
            # Preserve the required flag from the original descriptor
            if getattr(value, "required", False):
                annotations[key] = vo_cls
            else:
                annotations[key] = Optional[vo_cls]
                namespace[key] = None
            association_descriptors[key] = value

        elif isinstance(value, ResolvedField):
            finfo = model_field_info.get(key)
            if finfo:
                annotations[key] = finfo.annotation
                if finfo.default is not PydanticUndefined:
                    namespace[key] = finfo.default
                elif finfo.default_factory is not None:
                    namespace[key] = PydanticField(
                        default_factory=finfo.default_factory
                    )

    # Make identifier/unique fields optional (they are identity concerns,
    # not value concerns).
    container_fields = get_fields(entity_cls)
    for key in list(annotations.keys()):
        field_obj = container_fields.get(key)
        if isinstance(field_obj, ResolvedField) and (
            field_obj.identifier or field_obj.unique
        ):
            annotations[key] = annotations[key] | None
            namespace[key] = None

    ns = {"__annotations__": annotations, **namespace}
    value_object_cls = type(vo_name, (BaseValueObject,), ns)

    # Inject association descriptors into __container_fields__
    cf = getattr(value_object_cls, _FIELDS, {})
    cf.update(association_descriptors)
    setattr(value_object_cls, _FIELDS, cf)

    return value_object_cls