Skip to content

CIRPASS reference structure v1.3.0 — API reference

Status: Final (Phase 8 finalisation, 2026-05-09). Generated from the Pydantic models at src/dppvalidator/models/cirpass/v1_3/.

The CIRPASS reference structure v1.3.0 is the message-level wire format that the CIRPASS-2 project publishes alongside the EUDPP ontology. dppvalidator's Pydantic models are the source of truth; the JSON Schema bundled at schemas/data/cirpass-reference-1.3.0.json is derived from them via tools/codegen/cirpass/derive_schema.py.

Reading guide

Topic Page
Big-picture orientation cirpass-2-alignment.md
EUDPP module changelog eudpp-1.9-changelog.md
UNTP ↔ CIRPASS mapping untp-cirpass-mapping.md
Migration how-to migrate-untp-to-cirpass.md

Root: ReferencePassport

The CIRPASS DPP reference structure root. Maps to eudpp:DPP (P_DPP v1.9.1). Mirrors the v1.3.0 message tree-view shape: a Product at the root + sibling fields for the DPP-level metadata.

dppvalidator.models.cirpass.v1_3.ReferencePassport

Bases: UNTPBaseModel

The CIRPASS DPP reference structure root.

Maps to eudpp:DPP (P_DPP v1.9.1). Mirrors the v1.3.0 message tree-view shape: a Product at the root + sibling fields for the DPP-level metadata.

Wire shape (minimal): {"dppIdentifier": {...}, "product": <Product>, "issuedAt": <IssuedAt>}

Cardinality:

  • dpp_identifier: required (1) — uniquely identifies this DPP (distinct from the product's identifier).
  • product: required (1) — the Product the DPP describes.
  • issued_at: required (1) — when the DPP was issued.
  • effective_period: optional — explicit validity window.
  • related_actors: optional list of (actor, role) pairs.
  • actor_role_assignments: optional list of first-class role-assignment relationships (v1.9.1 ACTOR addition).
  • composition: optional — the product's material composition.
  • substances_of_concern: optional (0..n).
  • lca: optional — Life-Cycle Assessment results.
  • connector_relations: optional (0..n) cross-module relations.
  • previous_dpp: optional URI of the DPP this one supersedes (maps to eudpp:linkToPreviousDPP).
Source code in src/dppvalidator/models/cirpass/v1_3/passport.py
Python
class ReferencePassport(UNTPBaseModel):
    """The CIRPASS DPP reference structure root.

    Maps to ``eudpp:DPP`` (P_DPP v1.9.1). Mirrors the v1.3.0 message
    tree-view shape: a Product at the root + sibling fields for the
    DPP-level metadata.

    Wire shape (minimal):
        ``{"dppIdentifier": {...}, "product": <Product>,
           "issuedAt": <IssuedAt>}``

    Cardinality:

    - ``dpp_identifier``: required (1) — uniquely identifies *this*
      DPP (distinct from the product's identifier).
    - ``product``: required (1) — the Product the DPP describes.
    - ``issued_at``: required (1) — when the DPP was issued.
    - ``effective_period``: optional — explicit validity window.
    - ``related_actors``: optional list of (actor, role) pairs.
    - ``actor_role_assignments``: optional list of first-class
      role-assignment relationships (v1.9.1 ACTOR addition).
    - ``composition``: optional — the product's material composition.
    - ``substances_of_concern``: optional (0..n).
    - ``lca``: optional — Life-Cycle Assessment results.
    - ``connector_relations``: optional (0..n) cross-module relations.
    - ``previous_dpp``: optional URI of the DPP this one supersedes
      (maps to ``eudpp:linkToPreviousDPP``).
    """

    _jsonld_type: ClassVar[list[str]] = ["DigitalProductPassport", EUDPPClass.DPP.value]

    dpp_identifier: Identifier = Field(
        ...,
        alias="dppIdentifier",
        description="Unique identifier for this DPP instance (``eudpp:uniqueDPPID``).",
    )
    product: Product = Field(
        ...,
        description="The product this DPP describes.",
    )
    issued_at: IssuedAt = Field(
        ...,
        alias="issuedAt",
        description="When this DPP was issued.",
    )
    effective_period: EffectivePeriod | None = Field(
        default=None,
        alias="effectivePeriod",
        description="Validity window during which this DPP applies.",
    )
    related_actors: list[ActorRole] | None = Field(
        default=None,
        alias="relatedActors",
        description=(
            "Actors associated with this DPP and their roles "
            "(manufacturer, importer, recycler, etc.)."
        ),
    )
    actor_role_assignments: list[ActorRoleAssignment] | None = Field(
        default=None,
        alias="actorRoleAssignments",
        description=(
            "First-class actor-plays-role-in-context relationships "
            "(v1.9.1 ACTOR module). Use this when an assignment "
            "carries its own metadata (temporal scope, conferring "
            "authority, supporting documentation)."
        ),
    )
    composition: Composition | None = Field(
        default=None,
        description="Material composition of the product.",
    )
    substances_of_concern: list[SubstanceOfConcern] | None = Field(
        default=None,
        alias="substancesOfConcern",
        description="REACH / SVHC-tracked substances present in the product.",
    )
    lca: LifeCycleAssessment | None = Field(
        default=None,
        description="Life-Cycle Assessment / EPD results.",
    )
    connector_relations: list[ConnectorRelation] | None = Field(
        default=None,
        alias="connectorRelations",
        description="Cross-module typed relations (CON v1.9.1 predicates).",
    )
    previous_dpp: str | None = Field(
        default=None,
        alias="previousDpp",
        description=(
            "URI of the DPP this one supersedes "
            "(``eudpp:linkToPreviousDPP``). Enables version chains."
        ),
    )

options: show_source: false show_bases: false

Product / Identifier / Classification

dppvalidator.models.cirpass.v1_3.Product

Bases: UNTPBaseModel

A physical product placed on the EU market.

Maps to eudpp:Product (P_DPP v1.9.1). Carries the product's primary identifier, optional commodity classification, and multilingual product names.

Wire shape (minimal): {"productIdentifier": {...}, "productName": [{"value": "...", "language": "en"}]}

Cardinality:

  • productIdentifier: required (1).
  • productName: required (≥1) — at least one language must be provided. Multiple are encouraged for ESPR multilingual reach.
  • description: optional (0..1 list of LocalisedText).
  • commodityCode: optional (0..n list of ClassificationCode).
  • isComponentOf / isSparePartOf: optional self-references modelling the v1.9.1 transitive product hierarchy.
Source code in src/dppvalidator/models/cirpass/v1_3/product.py
Python
class Product(UNTPBaseModel):
    """A physical product placed on the EU market.

    Maps to ``eudpp:Product`` (P_DPP v1.9.1). Carries the product's
    primary identifier, optional commodity classification, and
    multilingual product names.

    Wire shape (minimal):
        ``{"productIdentifier": {...}, "productName": [{"value": "...", "language": "en"}]}``

    Cardinality:

    - ``productIdentifier``: required (1).
    - ``productName``: required (≥1) — at least one language must be
      provided. Multiple are encouraged for ESPR multilingual reach.
    - ``description``: optional (0..1 list of LocalisedText).
    - ``commodityCode``: optional (0..n list of ClassificationCode).
    - ``isComponentOf`` / ``isSparePartOf``: optional self-references
      modelling the v1.9.1 transitive product hierarchy.
    """

    _jsonld_type: ClassVar[list[str]] = ["Product", EUDPPClass.PRODUCT.value]

    product_identifier: Identifier = Field(
        ...,
        alias="productIdentifier",
        description="Primary product identifier (typically a GTIN or SPC URI).",
    )
    product_name: list[LocalisedText] = Field(
        ...,
        alias="productName",
        min_length=1,
        description="Product name(s), one entry per supported language.",
    )
    description: list[LocalisedText] | None = Field(
        default=None,
        description="Product description(s), one entry per language.",
    )
    commodity_code: list[ClassificationCode] | None = Field(
        default=None,
        alias="commodityCode",
        description="Commodity / taxonomy classifications for this product.",
    )
    is_component_of: list[str] | None = Field(
        default=None,
        alias="isComponentOf",
        description="URIs of products this product is a component of (transitive).",
    )
    is_spare_part_of: list[str] | None = Field(
        default=None,
        alias="isSparePartOf",
        description="URIs of products this product is a spare part of.",
    )

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.Identifier

Bases: UNTPBaseModel

A typed product or DPP identifier.

Wire shape

{"value": "01234567890128", "scheme": "https://gs1.org/voc/", "schemeName": "GS1 GTIN"}

value carries the identifier itself; scheme is the URI of the issuing authority's identifier register; schemeName is a short human label (e.g. GS1 GTIN, ISO/IEC 15459).

The scheme URI is what disambiguates collisions between identifier spaces (a 13-digit string can be a GTIN-13, an EAN-13, or something else entirely depending on the issuing authority).

Source code in src/dppvalidator/models/cirpass/v1_3/product.py
Python
class Identifier(UNTPBaseModel):
    """A typed product or DPP identifier.

    Wire shape:
        ``{"value": "01234567890128", "scheme": "https://gs1.org/voc/", "schemeName": "GS1 GTIN"}``

    ``value`` carries the identifier itself; ``scheme`` is the URI of
    the issuing authority's identifier register; ``schemeName`` is a
    short human label (e.g. ``GS1 GTIN``, ``ISO/IEC 15459``).

    The scheme URI is what disambiguates collisions between identifier
    spaces (a 13-digit string can be a GTIN-13, an EAN-13, or
    something else entirely depending on the issuing authority).
    """

    _jsonld_type: ClassVar[list[str]] = ["Identifier"]

    value: str = Field(..., min_length=1, description="The identifier value.")
    scheme: str = Field(
        ...,
        description=(
            "URI of the identifier scheme (the issuing authority's "
            "register, e.g. ``https://gs1.org/voc/``)."
        ),
    )
    scheme_name: str | None = Field(
        default=None,
        alias="schemeName",
        description="Human-readable scheme name (``GS1 GTIN``, ``ISO/IEC 15459``).",
    )

    @field_validator("scheme")
    @classmethod
    def _scheme_is_uri(cls, value: str) -> str:
        if not (value.startswith("http://") or value.startswith("https://")):
            msg = (
                f"Identifier.scheme must be an http(s) URI; got {value!r}. "
                f"The scheme URI is what disambiguates identifier spaces — "
                f"a bare label (e.g. ``GTIN``) is not enough."
            )
            raise ValueError(msg)
        return value

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.ClassificationCode

Bases: UNTPBaseModel

A taxonomy classification (commodity code, HS code, etc.).

Wire shape

{"code": "61091000", "scheme": "https://www.wcoomd.org/.../HS", "name": [{"value": "T-shirts...", "language": "en"}]}

Maps to eudpp:ClassificationCode (P_DPP v1.9.1). The CIRPASS spec uses this for HS / TARIC / commodity codes; the scheme URI tells consumers which taxonomy the code belongs to.

Source code in src/dppvalidator/models/cirpass/v1_3/product.py
Python
class ClassificationCode(UNTPBaseModel):
    """A taxonomy classification (commodity code, HS code, etc.).

    Wire shape:
        ``{"code": "61091000", "scheme": "https://www.wcoomd.org/.../HS",
           "name": [{"value": "T-shirts...", "language": "en"}]}``

    Maps to ``eudpp:ClassificationCode`` (P_DPP v1.9.1). The CIRPASS
    spec uses this for HS / TARIC / commodity codes; the ``scheme``
    URI tells consumers which taxonomy the code belongs to.
    """

    _jsonld_type: ClassVar[list[str]] = ["ClassificationCode"]

    code: str = Field(..., min_length=1, description="The classification code value.")
    scheme: str = Field(
        ...,
        description="URI of the classification scheme (HS, TARIC, CPV, …).",
    )
    name: list[LocalisedText] | None = Field(
        default=None,
        description="Human-readable description of the code, in one or more languages.",
    )

    @field_validator("scheme")
    @classmethod
    def _scheme_is_uri(cls, value: str) -> str:
        if not (value.startswith("http://") or value.startswith("https://")):
            msg = f"ClassificationCode.scheme must be an http(s) URI; got {value!r}."
            raise ValueError(msg)
        return value

options: show_source: false show_bases: false

Actor / Role

dppvalidator.models.cirpass.v1_3.Actor

Bases: UNTPBaseModel

An economic operator, regulator, or other party.

Maps to eudpp:Actor (ACTOR v1.9.1). Carries the actor's primary identifier, a multilingual name, and optional contact / address fields.

Wire shape (minimal): {"actorIdentifier": {...}, "actorName": [...]}

Source code in src/dppvalidator/models/cirpass/v1_3/actor.py
Python
class Actor(UNTPBaseModel):
    """An economic operator, regulator, or other party.

    Maps to ``eudpp:Actor`` (ACTOR v1.9.1). Carries the actor's
    primary identifier, a multilingual name, and optional contact /
    address fields.

    Wire shape (minimal):
        ``{"actorIdentifier": {...}, "actorName": [...]}``
    """

    _jsonld_type: ClassVar[list[str]] = ["Actor"]

    actor_identifier: Identifier = Field(
        ...,
        alias="actorIdentifier",
        description="Primary actor identifier (LEI, EUID, EORI, …).",
    )
    actor_name: list[LocalisedText] = Field(
        ...,
        alias="actorName",
        min_length=1,
        description="Actor's legal / trade name(s), one per language.",
    )
    registered_trade_name: list[LocalisedText] | None = Field(
        default=None,
        alias="registeredTradeName",
        description="Registered trade-name(s) where distinct from the legal name.",
    )
    registered_trademark: list[LocalisedText] | None = Field(
        default=None,
        alias="registeredTrademark",
        description="Registered trademark(s) associated with the actor.",
    )

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.Facility

Bases: UNTPBaseModel

A physical site where production / processing occurs.

Maps to eudpp:Facility (ACTOR v1.9.1 — relocated from P_DPP in the v1.9.1 spec rewrite). The CIRPASS message references a Facility via the actor's usesFacility relation; the facility itself carries an identifier scheme (so e.g. a permit ID and an ECEFACT ID for the same facility don't collide).

Source code in src/dppvalidator/models/cirpass/v1_3/actor.py
Python
class Facility(UNTPBaseModel):
    """A physical site where production / processing occurs.

    Maps to ``eudpp:Facility`` (ACTOR v1.9.1 — relocated from P_DPP in
    the v1.9.1 spec rewrite). The CIRPASS message references a Facility
    via the actor's ``usesFacility`` relation; the facility itself
    carries an identifier scheme (so e.g. a permit ID and an ECEFACT
    ID for the same facility don't collide).
    """

    _jsonld_type: ClassVar[list[str]] = ["Facility"]

    facility_identifier: Identifier = Field(
        ...,
        alias="facilityIdentifier",
        description="Primary facility identifier with scheme.",
    )
    facility_name: list[LocalisedText] = Field(
        ...,
        alias="facilityName",
        min_length=1,
        description="Facility name(s), one per language.",
    )

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.ActorRole

Bases: UNTPBaseModel

An actor playing a specific role on this DPP.

Wire shape

{"actor": <Actor>, "role": "eudpp:ManufacturerRole"}

The role field accepts the compact eudpp: IRI of any role class — both the v1.9.1 super-categories (EconomicOperatorRole, CircularEconomyRole, etc.) and the finer-grained ESPR-derived roles (ManufacturerRole, RecyclerRole, etc.) that :class:EUDPPRoleClass exposes for back-compat.

Source code in src/dppvalidator/models/cirpass/v1_3/actor.py
Python
class ActorRole(UNTPBaseModel):
    """An actor playing a specific role on this DPP.

    Wire shape:
        ``{"actor": <Actor>, "role": "eudpp:ManufacturerRole"}``

    The ``role`` field accepts the compact ``eudpp:`` IRI of any role
    class — both the v1.9.1 super-categories
    (``EconomicOperatorRole``, ``CircularEconomyRole``, etc.) and the
    finer-grained ESPR-derived roles
    (``ManufacturerRole``, ``RecyclerRole``, etc.) that
    :class:`EUDPPRoleClass` exposes for back-compat.
    """

    _jsonld_type: ClassVar[list[str]] = ["ActorRole"]

    actor: Actor = Field(..., description="The actor playing the role.")
    role: str = Field(
        ...,
        description=(
            "Compact ``eudpp:`` IRI of the role class. See "
            ":class:`dppvalidator.vocabularies.eudpp_actors.EUDPPRoleClass` "
            "for the canonical set."
        ),
    )

    @property
    def role_enum(self) -> EUDPPRoleClass | None:
        """Return the :class:`EUDPPRoleClass` member if ``role`` matches one.

        Returns ``None`` for roles outside the registered set
        (downstream taxonomies that extend the EUDPP role hierarchy
        with their own classes are still legal — :class:`ActorRole`
        carries the IRI string, not an enforced enum).
        """
        try:
            return EUDPPRoleClass(self.role)
        except ValueError:
            return None

role_enum property

Return the :class:EUDPPRoleClass member if role matches one.

Returns None for roles outside the registered set (downstream taxonomies that extend the EUDPP role hierarchy with their own classes are still legal — :class:ActorRole carries the IRI string, not an enforced enum).

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.ActorRoleAssignment

Bases: UNTPBaseModel

First-class actor-plays-role-in-context relationship (ACTOR v1.9.1).

+1.9.1: ACTOR v1.9.1 introduced eudpp:ActorRoleAssignment as a first-class entity (rather than a binary edge) so that role assignments can carry their own metadata — temporal scoping, authority that conferred the role, supporting documentation, etc.

Wire shape

{"actor": <Actor>, "role": "eudpp:...", "validFrom": "2026-01-01T00:00:00Z", "validTo": "2031-12-31T23:59:59Z"}

Source code in src/dppvalidator/models/cirpass/v1_3/actor.py
Python
class ActorRoleAssignment(UNTPBaseModel):
    """First-class actor-plays-role-in-context relationship (ACTOR v1.9.1).

    +1.9.1: ACTOR v1.9.1 introduced ``eudpp:ActorRoleAssignment`` as a
    first-class entity (rather than a binary edge) so that role
    assignments can carry their own metadata — temporal scoping,
    authority that conferred the role, supporting documentation, etc.

    Wire shape:
        ``{"actor": <Actor>, "role": "eudpp:...",
           "validFrom": "2026-01-01T00:00:00Z",
           "validTo": "2031-12-31T23:59:59Z"}``
    """

    _jsonld_type: ClassVar[list[str]] = ["ActorRoleAssignment"]

    actor: Actor = Field(..., description="The actor receiving the role.")
    role: str = Field(
        ...,
        description="Compact ``eudpp:`` IRI of the role class assigned.",
    )
    valid_from: str | None = Field(
        default=None,
        alias="validFrom",
        description=(
            "ISO 8601 UTC datetime from which the assignment is "
            "effective (``eudpp:assignmentValidFrom``)."
        ),
    )
    valid_to: str | None = Field(
        default=None,
        alias="validTo",
        description=(
            "ISO 8601 UTC datetime after which the assignment expires "
            "(``eudpp:assignmentValidTo``). Absent ⇒ open-ended."
        ),
    )

options: show_source: false show_bases: false

Material / Composition

dppvalidator.models.cirpass.v1_3.Material

Bases: UNTPBaseModel

A constituent material of a product.

Wire shape

{"materialName": [...], "materialType": "CO", "originCountry": "DE", "massFraction": 0.62, "isRecycled": true}

Cardinality:

  • materialName: required (≥1) — multilingual name of the material.
  • materialType: optional ISO 2076 fibre / material code (CO = Cotton, EL = Elastane, …). Bare-string per ISO convention; not wrapped in :class:LocalisedText.
  • originCountry: optional ISO-3166-1 alpha-2 country code.
  • massFraction: optional Decimal in [0, 1]; sum of all massFraction values across a product's materials should be ≤ 1.0 (validated cross-component, not on a single Material).
  • isRecycled: optional flag; True ⇒ material is reclaimed / post-consumer.
Source code in src/dppvalidator/models/cirpass/v1_3/material.py
Python
class Material(UNTPBaseModel):
    """A constituent material of a product.

    Wire shape:
        ``{"materialName": [...], "materialType": "CO",
           "originCountry": "DE", "massFraction": 0.62,
           "isRecycled": true}``

    Cardinality:

    - ``materialName``: required (≥1) — multilingual name of the
      material.
    - ``materialType``: optional ISO 2076 fibre / material code
      (``CO`` = Cotton, ``EL`` = Elastane, …). Bare-string per ISO
      convention; not wrapped in :class:`LocalisedText`.
    - ``originCountry``: optional ISO-3166-1 alpha-2 country code.
    - ``massFraction``: optional Decimal in [0, 1]; sum of all
      ``massFraction`` values across a product's materials should be
      ≤ 1.0 (validated cross-component, not on a single Material).
    - ``isRecycled``: optional flag; True ⇒ material is reclaimed /
      post-consumer.
    """

    _jsonld_type: ClassVar[list[str]] = ["Material"]

    material_name: list[LocalisedText] = Field(
        ...,
        alias="materialName",
        min_length=1,
        description="Material name(s), one per language.",
    )
    material_type: str | None = Field(
        default=None,
        alias="materialType",
        description="ISO 2076 textile-fibre code or equivalent material code.",
    )
    origin_country: str | None = Field(
        default=None,
        alias="originCountry",
        description="ISO-3166-1 alpha-2 country code of material origin.",
    )
    mass_fraction: Decimal | None = Field(
        default=None,
        alias="massFraction",
        ge=Decimal("0"),
        le=Decimal("1"),
        description="Mass fraction in [0, 1]; recycle/composition share.",
    )
    is_recycled: bool | None = Field(
        default=None,
        alias="isRecycled",
        description="True if the material is reclaimed / post-consumer recycled.",
    )

    @field_validator("material_type")
    @classmethod
    def _validate_material_type(cls, value: str | None) -> str | None:
        if value is None:
            return value
        if not _ISO_2076_RE.match(value):
            msg = (
                f"Material.materialType={value!r} is not a 2-letter ISO "
                f"2076 code (e.g. ``CO`` for Cotton). Use the bare ISO "
                f"code, not the human-readable name."
            )
            raise ValueError(msg)
        return value

    @field_validator("origin_country")
    @classmethod
    def _validate_origin_country(cls, value: str | None) -> str | None:
        if value is None:
            return value
        if not _ISO_3166_ALPHA2_RE.match(value):
            msg = f"Material.originCountry={value!r} is not a 2-letter ISO-3166-1 alpha-2 code."
            raise ValueError(msg)
        return value

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.Composition

Bases: UNTPBaseModel

The full material composition of a product.

Wire shape

{"materials": [<Material>, ...]}

The mass-fraction sum invariant is enforced at this level: across all entries with non-null massFraction, the sum must be ≤ 1.0 (slightly less than 1 is valid — non-mass-bearing constituents don't carry a fraction).

Source code in src/dppvalidator/models/cirpass/v1_3/material.py
Python
class Composition(UNTPBaseModel):
    """The full material composition of a product.

    Wire shape:
        ``{"materials": [<Material>, ...]}``

    The mass-fraction sum invariant is enforced at this level: across
    all entries with non-null ``massFraction``, the sum must be ≤ 1.0
    (slightly less than 1 is valid — non-mass-bearing constituents
    don't carry a fraction).
    """

    _jsonld_type: ClassVar[list[str]] = ["Composition"]

    materials: list[Material] = Field(
        ...,
        min_length=1,
        description="The materials that constitute the product.",
    )

    @model_validator(mode="after")
    def _mass_fractions_sum_within_one(self) -> Composition:
        total = sum(
            (m.mass_fraction for m in self.materials if m.mass_fraction is not None),
            start=Decimal("0"),
        )
        # Allow tiny floating-error tolerance — the spec is a sum of
        # Decimals so exact 1.0 is normal, but 0.9999999 rounding is
        # also seen in upstream feeds.
        if total > Decimal("1.0001"):
            msg = (
                f"Composition.materials mass fractions sum to {total} "
                f"(> 1.0). The sum across all materials with a non-null "
                f"massFraction must be ≤ 1.0."
            )
            raise ValueError(msg)
        return self

options: show_source: false show_bases: false

Substances of Concern

dppvalidator.models.cirpass.v1_3.SubstanceOfConcern

Bases: UNTPBaseModel

A REACH / SVHC-tracked substance present in a product.

Maps to eudpp:SubstanceOfConcern (SOC v1.9.1). Carries IUPAC / CAS / EC identifiers, hazard classifications, and one or more concentration measurements.

Wire shape (minimal): {"nameIUPAC": "...", "concentrations": [<Concentration>, ...]}

Cardinality:

  • At least one of nameIUPAC / nameCAS / numberCAS / numberEC must be present (substance must be identifiable).
  • concentrations: required (≥1).
  • hazards: optional (0..n).
Source code in src/dppvalidator/models/cirpass/v1_3/substances.py
Python
class SubstanceOfConcern(UNTPBaseModel):
    """A REACH / SVHC-tracked substance present in a product.

    Maps to ``eudpp:SubstanceOfConcern`` (SOC v1.9.1). Carries IUPAC /
    CAS / EC identifiers, hazard classifications, and one or more
    concentration measurements.

    Wire shape (minimal):
        ``{"nameIUPAC": "...", "concentrations": [<Concentration>, ...]}``

    Cardinality:

    - At least *one* of ``nameIUPAC`` / ``nameCAS`` / ``numberCAS`` /
      ``numberEC`` must be present (substance must be identifiable).
    - ``concentrations``: required (≥1).
    - ``hazards``: optional (0..n).
    """

    _jsonld_type: ClassVar[list[str]] = ["SubstanceOfConcern"]

    name_iupac: str | None = Field(
        default=None,
        alias="nameIUPAC",
        description="IUPAC systematic name.",
    )
    name_cas: str | None = Field(
        default=None,
        alias="nameCAS",
        description="CAS-registered name.",
    )
    number_cas: str | None = Field(
        default=None,
        alias="numberCAS",
        description="CAS Registry Number (e.g. ``71-43-2``).",
    )
    number_ec: str | None = Field(
        default=None,
        alias="numberEC",
        description="EC Number (e.g. ``200-753-7``).",
    )
    trade_name: list[LocalisedText] | None = Field(
        default=None,
        alias="tradeName",
        description="Trade name(s) under which the substance is marketed.",
    )
    concentrations: list[Concentration] = Field(
        ...,
        min_length=1,
        description="One or more concentration measurements for this substance.",
    )
    hazards: list[HazardClassification] | None = Field(
        default=None,
        description="Optional hazard classifications for this substance.",
    )

    @field_validator("number_cas")
    @classmethod
    def _validate_cas(cls, value: str | None) -> str | None:
        if value is not None and not _CAS_RE.match(value):
            msg = (
                f"SubstanceOfConcern.numberCAS={value!r} is not a "
                f"recognised CAS Registry Number format "
                f"(``<digits>-<2 digits>-<check digit>``, e.g. ``71-43-2``)."
            )
            raise ValueError(msg)
        return value

    @field_validator("number_ec")
    @classmethod
    def _validate_ec(cls, value: str | None) -> str | None:
        if value is not None and not _EC_RE.match(value):
            msg = (
                f"SubstanceOfConcern.numberEC={value!r} is not a "
                f"recognised EC Number format "
                f"(``<3 digits>-<3 digits>-<check digit>``, e.g. ``200-753-7``)."
            )
            raise ValueError(msg)
        return value

    def is_identified(self) -> bool:
        """True if at least one identifier (IUPAC / CAS / EC) is present."""
        return any(
            v is not None for v in (self.name_iupac, self.name_cas, self.number_cas, self.number_ec)
        )

is_identified()

True if at least one identifier (IUPAC / CAS / EC) is present.

Source code in src/dppvalidator/models/cirpass/v1_3/substances.py
Python
def is_identified(self) -> bool:
    """True if at least one identifier (IUPAC / CAS / EC) is present."""
    return any(
        v is not None for v in (self.name_iupac, self.name_cas, self.number_cas, self.number_ec)
    )

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.Concentration

Bases: UNTPBaseModel

The concentration of a substance of concern within a product / component.

Wire shape

{"value": 0.0008, "unit": "mass_fraction", "lifecycleStage": "in_product"}

Maps to eudpp:Concentration (SOC v1.9.1). The value is a Decimal in [0, 1] when unit is mass_fraction; for other units (e.g. mg_per_kg) the bound is on the unit's conventional range.

Source code in src/dppvalidator/models/cirpass/v1_3/substances.py
Python
class Concentration(UNTPBaseModel):
    """The concentration of a substance of concern within a product / component.

    Wire shape:
        ``{"value": 0.0008, "unit": "mass_fraction",
           "lifecycleStage": "in_product"}``

    Maps to ``eudpp:Concentration`` (SOC v1.9.1). The ``value`` is a
    Decimal in [0, 1] when ``unit`` is ``mass_fraction``; for other
    units (e.g. ``mg_per_kg``) the bound is on the unit's conventional
    range.
    """

    _jsonld_type: ClassVar[list[str]] = ["Concentration"]

    value: Decimal = Field(
        ...,
        ge=Decimal("0"),
        description="Concentration value in the unit identified by ``unit``.",
    )
    unit: str = Field(
        ...,
        description="Unit of measurement (``mass_fraction``, ``mg_per_kg``, …).",
    )
    lifecycle_stage: LifeCycleStage | None = Field(
        default=None,
        alias="lifecycleStage",
        description="Lifecycle stage at which this concentration applies.",
    )

    @field_validator("value")
    @classmethod
    def _bound_when_mass_fraction(cls, value: Decimal) -> Decimal:
        # Mass-fraction-specific upper bound (1.0) is enforced by a
        # cross-field validator below; this validator just ensures
        # the value is non-negative regardless of unit.
        return value

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.HazardClassification

Bases: UNTPBaseModel

A single hazard classification per CLP / Regulation (EC) 1272/2008.

Wire shape

{"category": "carcinogenicity", "statement": [{"value": "May cause cancer.", "language": "en"}]}

The category field accepts any value from :class:dppvalidator.vocabularies.eudpp_substances.HazardCategory; new categories added in a future SOC release land in that enum, not here.

Source code in src/dppvalidator/models/cirpass/v1_3/substances.py
Python
class HazardClassification(UNTPBaseModel):
    """A single hazard classification per CLP / Regulation (EC) 1272/2008.

    Wire shape:
        ``{"category": "carcinogenicity",
           "statement": [{"value": "May cause cancer.", "language": "en"}]}``

    The ``category`` field accepts any value from
    :class:`dppvalidator.vocabularies.eudpp_substances.HazardCategory`;
    new categories added in a future SOC release land in that enum,
    not here.
    """

    _jsonld_type: ClassVar[list[str]] = ["HazardClassification"]

    category: HazardCategory = Field(
        ...,
        description=(
            "CLP / SVHC hazard category (carcinogenicity, mutagenicity, "
            "reproductive toxicity, PBT, vPvB, PMT, vPvM, …)."
        ),
    )
    statement: list[LocalisedText] | None = Field(
        default=None,
        description=("Optional H-statement text(s) describing the hazard, one per language."),
    )

options: show_source: false show_bases: false

Life-Cycle Assessment

dppvalidator.models.cirpass.v1_3.LifeCycleAssessment

Bases: UNTPBaseModel

A complete LCA / EPD attached to a product.

Maps to eudpp:LCAStudy (LCA v1.9.4-Maki). Carries one or more impact results plus optional methodology metadata. Phase 4 wires SHACL validation of the impact-result set against the bundled LCA shape graph.

Wire shape (minimal): {"results": [<ImpactResult>, ...]}

Cardinality:

  • results: required (≥1).
  • methodology: optional — short label (PEF 3.1, EN 15804+A2, …) or compact IRI of an :class:dppvalidator.vocabularies.eudpp_lca.LCAClass LCIAMethodology instance.
  • referencePeriod: optional — when the LCA result was computed (production batch, study window, etc.).
Source code in src/dppvalidator/models/cirpass/v1_3/lca.py
Python
class LifeCycleAssessment(UNTPBaseModel):
    """A complete LCA / EPD attached to a product.

    Maps to ``eudpp:LCAStudy`` (LCA v1.9.4-Maki). Carries one or more
    impact results plus optional methodology metadata. Phase 4 wires
    SHACL validation of the impact-result set against the bundled
    LCA shape graph.

    Wire shape (minimal):
        ``{"results": [<ImpactResult>, ...]}``

    Cardinality:

    - ``results``: required (≥1).
    - ``methodology``: optional — short label (``PEF 3.1``,
      ``EN 15804+A2``, …) or compact IRI of an
      :class:`dppvalidator.vocabularies.eudpp_lca.LCAClass`
      ``LCIAMethodology`` instance.
    - ``referencePeriod``: optional — when the LCA result was
      computed (production batch, study window, etc.).
    """

    _jsonld_type: ClassVar[list[str]] = ["LifeCycleAssessment"]

    results: list[ImpactResult] = Field(
        ...,
        min_length=1,
        description="The quantified impact results.",
    )
    methodology: str | None = Field(
        default=None,
        description=(
            "LCA methodology label or compact IRI (``PEF 3.1``, ``eudpp:LCIAMethodology``, …)."
        ),
    )
    reference_period: EffectivePeriod | None = Field(
        default=None,
        alias="referencePeriod",
        description="The temporal scope of the LCA results.",
    )

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.ImpactResult

Bases: UNTPBaseModel

A single quantified environmental impact result.

Wire shape

{"impactCategory": <ImpactCategoryReference>, "value": 12.34, "unit": "kg_CO2_eq"}

Pairs an impact-category reference with a numeric value + unit. The unit field is a free-string for now (PEF 3.1 conventions use shorthand like kg_CO2_eq, CTUe); UNECE Rec20 / QUDT normalisation is enforced at the validator layer (Phase 4 LCS rule pack).

Source code in src/dppvalidator/models/cirpass/v1_3/lca.py
Python
class ImpactResult(UNTPBaseModel):
    """A single quantified environmental impact result.

    Wire shape:
        ``{"impactCategory": <ImpactCategoryReference>,
           "value": 12.34, "unit": "kg_CO2_eq"}``

    Pairs an impact-category reference with a numeric value + unit.
    The ``unit`` field is a free-string for now (PEF 3.1 conventions
    use shorthand like ``kg_CO2_eq``, ``CTUe``); UNECE Rec20 / QUDT
    normalisation is enforced at the validator layer (Phase 4 LCS
    rule pack).
    """

    _jsonld_type: ClassVar[list[str]] = ["ImpactResult"]

    impact_category: ImpactCategoryReference = Field(
        ...,
        alias="impactCategory",
        description="The PEF / EN 15804 impact category quantified.",
    )
    value: Decimal = Field(..., description="The numeric impact value.")
    unit: str = Field(
        ...,
        description=(
            "Unit of measurement (``kg_CO2_eq``, ``CTUe``, …). PEF 3.1 "
            "conventions; Phase 4 enforces UNECE Rec20 / QUDT alignment."
        ),
    )

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.ImpactCategoryReference

Bases: UNTPBaseModel

A reference to a PEF / EN 15804 impact category.

Wire shape

{"category": "eudpp:climateChange", "name": [{"value": "Climate change", "language": "en"}]}

The category field is a compact eudpp: IRI from :class:dppvalidator.vocabularies.eudpp_lca.LCIAImpactCategory (the v1.9.4-Maki canonical set: 16 PEF/EN15804+A2 individuals). Legacy v2.0 lca: IRIs are tolerated here for back-compat with UNTP-sourced payloads — the Phase 5 mapping shim translates them.

Source code in src/dppvalidator/models/cirpass/v1_3/lca.py
Python
class ImpactCategoryReference(UNTPBaseModel):
    """A reference to a PEF / EN 15804 impact category.

    Wire shape:
        ``{"category": "eudpp:climateChange",
           "name": [{"value": "Climate change", "language": "en"}]}``

    The ``category`` field is a compact ``eudpp:`` IRI from
    :class:`dppvalidator.vocabularies.eudpp_lca.LCIAImpactCategory`
    (the v1.9.4-Maki canonical set: 16 PEF/EN15804+A2 individuals).
    Legacy v2.0 ``lca:`` IRIs are tolerated here for back-compat with
    UNTP-sourced payloads — the Phase 5 mapping shim translates them.
    """

    _jsonld_type: ClassVar[list[str]] = ["ImpactCategoryReference"]

    category: str = Field(
        ...,
        description=(
            "Compact ``eudpp:`` IRI of the impact category (canonical "
            "v1.9.4-Maki). Legacy ``lca:`` IRIs accepted for back-compat."
        ),
    )
    name: list[LocalisedText] | None = Field(
        default=None,
        description="Human-readable display name(s) of the impact category.",
    )

    @property
    def category_enum(self) -> LCIAImpactCategory | None:
        """Return the :class:`LCIAImpactCategory` member if ``category`` matches one.

        Returns ``None`` for category IRIs outside the canonical
        v1.9.4-Maki set (e.g. legacy ``lca:`` IRIs, or downstream
        taxonomies that extend the impact-category hierarchy).
        """
        try:
            return LCIAImpactCategory(self.category)
        except ValueError:
            return None

category_enum property

Return the :class:LCIAImpactCategory member if category matches one.

Returns None for category IRIs outside the canonical v1.9.4-Maki set (e.g. legacy lca: IRIs, or downstream taxonomies that extend the impact-category hierarchy).

options: show_source: false show_bases: false

Connector relations

dppvalidator.models.cirpass.v1_3.ConnectorRelation

Bases: UNTPBaseModel

A typed cross-module relation between two entities.

Wire shape

{"relation": "eudpp:hasManufacturer", "subject": "https://example.com/dpp/123", "object": "https://example.com/actor/abc"}

The relation field is a compact eudpp: IRI from :class:RelationType (or any other ontology-defined object property). subject and object are URIs identifying the related entities.

Cardinality:

  • relation: required (1).
  • subject: required (1).
  • object: required (1).
  • valid_from / valid_to: optional ISO 8601 datetimes — enable temporally-scoped relations (e.g. an actor that played a role only during a manufacturing batch).
Source code in src/dppvalidator/models/cirpass/v1_3/connector.py
Python
class ConnectorRelation(UNTPBaseModel):
    """A typed cross-module relation between two entities.

    Wire shape:
        ``{"relation": "eudpp:hasManufacturer",
           "subject": "https://example.com/dpp/123",
           "object": "https://example.com/actor/abc"}``

    The ``relation`` field is a compact ``eudpp:`` IRI from
    :class:`RelationType` (or any other ontology-defined object
    property). ``subject`` and ``object`` are URIs identifying the
    related entities.

    Cardinality:

    - ``relation``: required (1).
    - ``subject``: required (1).
    - ``object``: required (1).
    - ``valid_from`` / ``valid_to``: optional ISO 8601 datetimes —
      enable temporally-scoped relations (e.g. an actor that played
      a role only during a manufacturing batch).
    """

    _jsonld_type: ClassVar[list[str]] = ["ConnectorRelation"]

    relation: str = Field(
        ...,
        description=(
            "Compact ``eudpp:`` IRI of the relation predicate. See "
            ":class:`RelationType` for the canonical set; downstream "
            "taxonomies may use other IRIs."
        ),
    )
    subject: str = Field(
        ...,
        min_length=1,
        description="URI of the subject entity (the relation's source).",
    )
    object: str = Field(
        ...,
        min_length=1,
        description="URI of the object entity (the relation's target).",
    )
    valid_from: str | None = Field(
        default=None,
        alias="validFrom",
        description="Optional ISO 8601 datetime: relation effective from.",
    )
    valid_to: str | None = Field(
        default=None,
        alias="validTo",
        description="Optional ISO 8601 datetime: relation expires at.",
    )

    @field_validator("subject", "object")
    @classmethod
    def _looks_like_uri(cls, value: str) -> str:
        # We don't enforce strict URI shape (downstream consumers may
        # legitimately use compact ``eudpp:Foo`` form), but we reject
        # bare-string values that obviously aren't identifiers.
        if not value.strip():
            msg = "ConnectorRelation subject/object must not be empty."
            raise ValueError(msg)
        return value

    @property
    def relation_type(self) -> RelationType | None:
        """Return the :class:`RelationType` member if ``relation`` matches one.

        Returns ``None`` for relation IRIs outside the canonical set
        (downstream extensions are valid — :class:`ConnectorRelation`
        carries the IRI string, not an enforced enum).
        """
        try:
            return RelationType(self.relation)
        except ValueError:
            return None

relation_type property

Return the :class:RelationType member if relation matches one.

Returns None for relation IRIs outside the canonical set (downstream extensions are valid — :class:ConnectorRelation carries the IRI string, not an enforced enum).

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.RelationType

Bases: str, Enum

Known cross-module relation predicates (CIRPASS-2 CON v1.9.1).

Each member is a compact eudpp: IRI for a CON-module object property. Downstream consumers that emit :class:ConnectorRelation payloads with relation predicates outside this enum (e.g. pilot-specific extensions) are still valid — :class:ConnectorRelation carries the IRI as a string, not as an enforced enum.

Source code in src/dppvalidator/models/cirpass/v1_3/connector.py
Python
class RelationType(str, Enum):
    """Known cross-module relation predicates (CIRPASS-2 CON v1.9.1).

    Each member is a compact ``eudpp:`` IRI for a CON-module object
    property. Downstream consumers that emit :class:`ConnectorRelation`
    payloads with relation predicates *outside* this enum (e.g.
    pilot-specific extensions) are still valid — :class:`ConnectorRelation`
    carries the IRI as a string, not as an enforced enum.
    """

    # CON-native relations (added to v1.9.1 connector module)
    IS_CONNECTED_TO = "eudpp:isConnectedTo"
    IN_CONTEXT_OF_ACTIVITY = "eudpp:inContextOfActivity"
    IN_CONTEXT_OF_DPP = "eudpp:inContextOfDPP"
    IN_CONTEXT_OF_PRODUCT = "eudpp:inContextOfProduct"
    REPRESENTS_MANUFACTURER_FOR_PRODUCT = "eudpp:representsManufacturerForProduct"

    # Migrated to CON in v1.9.1 (were in P_DPP v1.7.1)
    HAS_ISSUER = "eudpp:hasIssuer"
    HAS_MANUFACTURER = "eudpp:hasManufacturer"
    HAS_ECONOMIC_OPERATOR = "eudpp:hasEconomicOperator"
    HAS_BACK_UP_COPY_HOST = "eudpp:hasBackUpCopyHost"
    CONTAINS_SUBSTANCE_OF_CONCERN = "eudpp:containsSubstanceOfConcern"

options: show_source: false show_bases: false

Multilingual labels

dppvalidator.models.cirpass.v1_3.LocalisedText

Bases: UNTPBaseModel

A text value tagged with a BCP-47 language identifier.

Wire shape: {"value": "Cotton t-shirt", "language": "en"}.

Used wherever the CIRPASS reference structure expects a user-facing label that must surface in multiple official languages — product name, description, classification labels, etc. The set of fields requiring this wrapper is dictated by the v1.9.1 SHACL shape audit (Phase 1 task 1.6); fields without the multilingual requirement use plain :class:str.

Attributes:

Name Type Description
value str

The text content. Whitespace is trimmed by the base model's str_strip_whitespace config.

language str

BCP-47 tag (en, de, zh-Hant, en-US, etc.). Validated against the pragmatic-subset grammar in :data:_BCP47_RE. Lower-case primary subtag, initial-case script subtag, upper-case region subtag.

Source code in src/dppvalidator/models/cirpass/v1_3/i18n.py
Python
class LocalisedText(UNTPBaseModel):
    """A text value tagged with a BCP-47 language identifier.

    Wire shape: ``{"value": "Cotton t-shirt", "language": "en"}``.

    Used wherever the CIRPASS reference structure expects a
    user-facing label that must surface in multiple official
    languages — product name, description, classification labels,
    etc. The set of fields requiring this wrapper is dictated by the
    v1.9.1 SHACL shape audit (Phase 1 task 1.6); fields without the
    multilingual requirement use plain :class:`str`.

    Attributes:
        value: The text content. Whitespace is trimmed by the base
            model's ``str_strip_whitespace`` config.
        language: BCP-47 tag (``en``, ``de``, ``zh-Hant``,
            ``en-US``, etc.). Validated against the pragmatic-subset
            grammar in :data:`_BCP47_RE`. Lower-case primary subtag,
            initial-case script subtag, upper-case region subtag.
    """

    _jsonld_type: ClassVar[list[str]] = ["LocalisedText"]

    value: str = Field(
        ...,
        description="The text value in the language identified by ``language``.",
        min_length=1,
    )
    language: str = Field(
        ...,
        description="BCP-47 language tag (e.g. ``en``, ``de``, ``zh-Hant``).",
    )

    @field_validator("language")
    @classmethod
    def _validate_bcp47(cls, value: str) -> str:
        if not _BCP47_RE.match(value):
            msg = (
                f"language tag {value!r} is not a recognised BCP-47 form. "
                f"Expected ``primary[-Script][-REGION][-variant]``, e.g. "
                f"``en``, ``de``, ``zh-Hant``, ``en-GB``."
            )
            raise ValueError(msg)
        return value

options: show_source: false show_bases: false

Temporal

dppvalidator.models.cirpass.v1_3.EffectivePeriod

Bases: UNTPBaseModel

The validity window during which a CIRPASS DPP applies.

Wire shape

{"start": "2026-01-01T00:00:00Z", "end": "2031-12-31T23:59:59Z"}

Both endpoints are ISO 8601 datetimes with UTC offset. start is required; end is optional (a DPP with no end date is effective indefinitely until superseded). When both are present, start <= end is enforced via a :func:@model_validator.

Maps to UNTP 0.7's validFrom / validUntil envelope fields via the Phase 5 compat shim.

Source code in src/dppvalidator/models/cirpass/v1_3/temporal.py
Python
class EffectivePeriod(UNTPBaseModel):
    """The validity window during which a CIRPASS DPP applies.

    Wire shape:
        ``{"start": "2026-01-01T00:00:00Z", "end": "2031-12-31T23:59:59Z"}``

    Both endpoints are ISO 8601 datetimes with UTC offset. ``start``
    is required; ``end`` is optional (a DPP with no end date is
    effective indefinitely until superseded). When both are present,
    ``start <= end`` is enforced via a :func:`@model_validator`.

    Maps to UNTP 0.7's ``validFrom`` / ``validUntil`` envelope fields
    via the Phase 5 compat shim.
    """

    _jsonld_type: ClassVar[list[str]] = ["EffectivePeriod"]

    start: datetime = Field(
        ...,
        description="UTC datetime from which the DPP is effective.",
    )
    end: datetime | None = Field(
        default=None,
        description=(
            "UTC datetime after which the DPP is no longer effective. "
            "Absent ⇒ effective indefinitely."
        ),
    )

    @model_validator(mode="after")
    def _start_before_end(self) -> EffectivePeriod:
        if self.end is not None and self.start > self.end:
            msg = (
                f"EffectivePeriod.start={self.start.isoformat()} is later "
                f"than .end={self.end.isoformat()}; the period is empty. "
                f"If the DPP is no longer effective, drop ``end`` and "
                f"supersede with a new DPP instead."
            )
            raise ValueError(msg)
        return self

options: show_source: false show_bases: false

dppvalidator.models.cirpass.v1_3.IssuedAt

Bases: UNTPBaseModel

Timestamp at which a CIRPASS DPP was issued.

Wire shape: {"timestamp": "2026-01-15T09:30:00Z"}.

Maps to UNTP 0.7's issuanceDate envelope field via the Phase 5 compat shim. Carries an explicit type-wrapper rather than a bare datetime so that downstream LD payloads can attach IssuedAt-scoped metadata (e.g. issuing-software, signature) on the same node.

Source code in src/dppvalidator/models/cirpass/v1_3/temporal.py
Python
class IssuedAt(UNTPBaseModel):
    """Timestamp at which a CIRPASS DPP was issued.

    Wire shape: ``{"timestamp": "2026-01-15T09:30:00Z"}``.

    Maps to UNTP 0.7's ``issuanceDate`` envelope field via the Phase 5
    compat shim. Carries an explicit type-wrapper rather than a bare
    datetime so that downstream LD payloads can attach
    ``IssuedAt``-scoped metadata (e.g. issuing-software, signature) on
    the same node.
    """

    _jsonld_type: ClassVar[list[str]] = ["IssuedAt"]

    timestamp: datetime = Field(
        ...,
        description="UTC datetime of issuance.",
    )

    @field_validator("timestamp")
    @classmethod
    def _require_tz_aware(cls, value: datetime) -> datetime:
        if value.tzinfo is None:
            msg = (
                "IssuedAt.timestamp must be timezone-aware (typically UTC). "
                "Naïve datetimes risk ambiguous interpretation across "
                "issuer / consumer locales."
            )
            raise ValueError(msg)
        return value

options: show_source: false show_bases: false