Written by

Sales Engineer at InterSystems
Article Guillaume Rongier · 8 hr ago 9m read

Native IRIS messages in IoP with PersistentMessage and iris-persistence

 

In the previous IoP article, I showed how IoP can expose Python messages to DTL by generating JSON schemas. That is useful when the message is primarily a Python object and we want the IRIS tooling to understand its structure.

This time, the direction is a little different.

Starting with IoP 3.7.1, a PersistentMessage can now be a native IRIS message body class. The Python class is still the source code you write, but the generated IRIS class extends Ens.MessageBody, has real IRIS properties, and can be used directly by ObjectScript interoperability components, message maps, DTL, SQL projection, and the Management Portal.

The feature is built on top of iris-persistence, the Python-first persistence layer for InterSystems IRIS introduced in this article.

Why another message type?

IoP already had several message styles:

  • Message, usually a Python dataclass serialized into an IoP message wrapper.
  • PydanticMessage, useful when validation and JSON schema generation are important.
  • PickleMessage, useful for Python-to-Python scenarios.

These are still valid, and they are often the simplest choice.

PersistentMessage is for another case: when the message should be a real IRIS interoperability message body.

That matters when:

  • an ObjectScript business process or business operation must receive the message directly;
  • a MessageMap should dispatch on the generated IRIS class;
  • a DTL should work with native IRIS properties instead of an IoP wrapper payload;
  • the message should participate in the IRIS object model more naturally;
  • Python and ObjectScript components need to share the same message contract.

A first PersistentMessage

Here is a simple message class:

# msg.py
from iop import Field, PersistentMessage


class OrderMessage(PersistentMessage):
    OrderId: str = Field(required=True, max_length=64)
    Amount: float = 0.0

This looks like a normal Python class, but it is not only a Python serialization contract.

When the schema is synchronized, IoP uses iris-persistence to generate an IRIS class. By default, that class extends Ens.MessageBody.

The resulting message can be sent through an interoperability production as a native IRIS object.

Registering the message

As with other IoP components, registration happens in settings.py.

For PersistentMessage, the key in CLASSES is the IRIS class name to generate:

# settings.py
from msg import OrderMessage


CLASSES = {
    "Demo.Msg.OrderMessage": OrderMessage,
}

Then run the migration:

iop --migrate /path/to/settings.py

During migration, IoP synchronizes the IRIS class and stores metadata parameters on it. Those parameters tell IoP which Python class corresponds to the native IRIS class, even when the message was created first by ObjectScript or by an IRIS interoperability component.

Using it in a Python business operation

Once the message is registered, Python components can use it directly.

# bo.py
from iop import BusinessOperation

from msg import OrderMessage


class OrderOperation(BusinessOperation):
    def on_order(self, request: OrderMessage):
        self.log_info(f"Order {request.OrderId}: {request.Amount}")

        request.Amount = request.Amount + 1
        return request

IoP already inspects typed handler methods. In this example, on_order() is selected for OrderMessage requests because its argument is annotated with the message class.

On the way into IRIS, IoP materializes the Python object as a native Demo.Msg.OrderMessage instance. On the way back into Python, IoP recognizes the IRIS class and rehydrates the Python OrderMessage.

Using the same message in ObjectScript

Because the generated class is a native IRIS message body, ObjectScript components can also dispatch on it.

For example, a business operation can add it to a message map:

XData MessageMap
{
<MapItems>
    <MapItem MessageType="Demo.Msg.OrderMessage">
        <Method>HandleOrder</Method>
    </MapItem>
</MapItems>
}

And the method receives a normal IRIS object:

Method HandleOrder(
    pRequest As Demo.Msg.OrderMessage,
    Output pResponse As Ens.MessageBody) As %Status
{
    Set tStatus = $$$OK

    Try {
        Set pResponse = pRequest
        Set pResponse.Amount = pRequest.Amount + 1
    }
    Catch ex {
        Set tStatus = ex.AsStatus()
    }

    Quit tStatus
}

This is the main difference from the older IoP message wrappers: the ObjectScript side does not need to decode a Python payload to see the message fields.

Compared with iris.cls and native IRIS messages

It is important to position PersistentMessage correctly. It does not replace iris.cls, and it does not replace hand-written ObjectScript message classes. It sits between both approaches.

Using iris.cls

With Embedded Python or the Native API, you can already use iris.cls to create and manipulate an IRIS object directly:

import iris


order = iris.cls("Demo.Msg.OrderMessage")._New()
order.OrderId = "A-1000"
order.Amount = 42.0

This is the most direct bridge to IRIS. It is useful when the class already exists and IRIS owns the model.

But iris.cls does not give you a Python message contract. You still have to know the IRIS class name, property names, object lifecycle, and conversion rules. In an IoP component, you also have to decide manually how to map that native object to a Python type if you want typed handlers and Python-side validation.

Use iris.cls when you want low-level control over an existing IRIS class from Python.

Use PersistentMessage when you want the Python class to define the interoperability message contract, while still generating and using a native IRIS class.

Hand-written native IRIS messages

You can also write the message class directly in ObjectScript:

Class Demo.Msg.OrderMessage Extends Ens.MessageBody
{
Property OrderId As %String(MAXLEN = 64) [ Required ];

Property Amount As %Double;
}

This is the traditional IRIS-first approach. It is the right choice when ObjectScript should own the schema, when you need custom ObjectScript behavior on the message class, or when the class is already part of an existing production.

In that case, Python can still receive the native object, but without PersistentMessage it remains an IRIS object wrapper from the Python point of view. You can access request.OrderId, but IoP will not automatically rehydrate it into a typed Python message class unless there is a PersistentMessage mapping.

Summary

Approach Source of truth Python experience IRIS experience Best fit
iris.cls Existing IRIS class Low-level native object access Fully native Calling or manipulating existing IRIS classes from Python
Hand-written native message ObjectScript class Native object wrapper unless manually mapped Fully native IRIS-first productions and brownfield message models
PersistentMessage Python class Typed Python message Generated native Ens.MessageBody Python-first IoP components that must interoperate naturally with IRIS

So the choice is not only technical. It is mostly about ownership.

If IRIS owns the class, use iris.cls or a hand-written native message. If Python owns the message contract but IRIS must see a real message body, use PersistentMessage.

Nested objects

PersistentMessage can also use iris-persistence models for nested native objects.

For example:

from iop import Field, Model, PersistentMessage


class Address(Model, serial=True):
    City: str = Field(required=True, max_length=80)
    PostalCode: str | None = None


class OrderMessage(PersistentMessage):
    OrderId: str = Field(required=True, max_length=64)
    Amount: float = 0.0
    ShipTo: Address | None = None

Here Address is a %SerialObject model and OrderMessage remains the native interoperability message body. This makes it possible to keep a Python-first declaration style while still generating IRIS classes that match the object model.

What iris-persistence brings here

iris-persistence is doing the persistence work behind the scenes:

  • it maps Python type hints to IRIS property types;
  • it writes class, property, parameter, index, and storage metadata into the IRIS dictionary;
  • it compiles the generated class;
  • it materializes Python objects into IRIS objects;
  • it rehydrates Python objects from native IRIS objects.

For PersistentMessage, IoP adds the interoperability-specific behavior:

  • default superclass: Ens.MessageBody;
  • default schema synchronization mode: extend;
  • CLASSES registration using the IRIS class name;
  • message metadata parameters such as IOP_MESSAGE_KIND, IOP_PYTHON_CLASS, and IOP_PYTHON_CLASSPATH;
  • automatic serialization and deserialization in the existing IoP dispatch pipeline.

The default extend mode is intentional. It allows Python to add or update the fields it declares without deleting unrelated IRIS-side members. That is usually the safest default for interoperability projects where Python and ObjectScript may coexist.

Installation notes

The current IoP package metadata includes iris-persistence as a dependency.

For a fresh installation:

pip install --upgrade iris-pex-embedded-python

For older server-side environments, especially remote IRIS deployments where the Python package was installed manually, install iris-persistence explicitly in the IRIS Python environment:

python3 -m pip install "iris-persistence>=0.1.1"

Remember that iop -i installs or updates the ObjectScript support classes, but the Python packages must also exist in the Python environment used by IRIS.

When should I use it?

Use PersistentMessage when the message contract should be native to IRIS.

Use Message or PydanticMessage when the message mainly lives in Python and you only need serialization, validation, or JSON-schema-based DTL support.

In other words:

  • Python-to-Python flow: Message, PydanticMessage, or PickleMessage may be enough.
  • Python and ObjectScript sharing the same message body: PersistentMessage is a better fit.
  • Native DTL or ObjectScript MessageMap over typed IRIS properties: PersistentMessage.

Conclusion

This feature connects two Python-first projects:

  • IoP, for building IRIS interoperability productions in Python;
  • iris-persistence, for generating and manipulating native IRIS persistent objects from Python.

The result is a message class that can be written in Python, synchronized as an IRIS class, passed through an interoperability production, handled by Python or ObjectScript, and materialized back into Python without losing its native IRIS identity.

It is a small API surface, but it changes the integration story: Python components no longer have to choose between a convenient Python message and a native IRIS message body. With PersistentMessage, they can have both.

Feedback would be especially useful on:

  • brownfield interoperability productions that already have ObjectScript message bodies;
  • DTL use cases where native generated classes are easier than JSON schema wrappers;
  • how much of the underlying iris-persistence metadata should be exposed directly in IoP.