Serialization and Deserialization of Widget Attributes

Properties of xobjects are automatically synchronized with the counterpart front-end object. The synchronization involves the serialization and deserialization of the modified properties.

Standard case: JSON serialization of properties

By default, if the type held by the property is JSON-serializable, the behavior of xwidgets will be to make use of the JSON serialization for that type to synchronize the property value between the kernel and the front-end.

By JSON-serializable, we mean that the type has to be convertible from and to the json type of the nlohmann_json package, a.k.a “JSON for Modern C++”. Integral types, floating points, and standard STL containers are supported by nlohmann_json.

JSON serialization and deserialization for a user-defined type can be specified by providing an overload of the to_json and from_json functions in the same namespace where it is defined.

For example, the serialization of a type ns::person with attributes “name”, “address” and “age” can be specified in the following fashion:

using nlohmann::json;

namespace ns
{
    void to_json(json& j, const person& p)
    {
        j = json{{"name", p.name}, {"address", p.address}, {"age", p.age}};
    }

    void from_json(const json& j, person& p)
    {
        p.name = j.at("name").get<std::string>();
        p.address = j.at("address").get<std::string>();
        p.age = j.at("age").get<int>();
    }
}

Upon serialization and deserialization of ns::person objects, the overloads of to_json and from_json are picked up by argument-dependent lookup (ADL).

Eventually, patches sent to and received from the front-end are JSON objects whose keys are the names of the attributes, and the values are the JSON representations of the new value.

For example, the JSON patch for a slider widget corresponding to a change of attributes “value”, “min”, “readout_format” looks like:

{
    "value" : 10,
    "min": 0,
    "readout_format": ".2f"
}

Advanced use cases: making use of binary serialization

The Jupyter Widgets communication protocol allows for the communication of raw binary buffers to the front-end. This is especially convenient in visualization packages where large numerical arrays may be sent across the wire, or when serializing images and such.

The Jupyter binary serialization protocol

Upon modification of properties in the front-end or the back-end, the content of the comm message sent to or from the front-end is

  • a JSON patch holding the JSON-serialized values of the modified attributes

  • optionally a number of binary frames holding raw data.

On the JavaScript side, the first stage of the deserialization consists in

  1. deserializing the JSON into nested JavaScript objects and arrays.

  2. inserting the binary frames in the deserialized JSON in the form of DataView objects at locations specified in a companion buffer_paths array sent across the wire alongside the list of buffers.

For example, in the image widget, the value attribute holds the binary data for the image (encoded in the format specified with the format string attribute). A patch for a change if the value and format attribute will look like

// JSON patch:

{
    "format": "png"
}

// buffer paths
[["value"]]

// Buffers
[ { -- Binary png buffer -- } ]

On the JavaSCript side, this gets deserialized into

{
    "format": "png",
    "value": DataView({ -- Binary png buffer -- })}
}

On the C++ side, the internal machinery of xwidgets automatically composes this list of paths for the user. Custom widget authors must compose a message of the form

// JSON patch:

{
    "format": "png",
    "value": "@buffer_reference@0"
}

// Buffers
[ { -- Binary png buffer -- } ]

Instead of specifying the buffer paths in a separate array, the location where the buffer is to be inserted holds a placeholder string indicating the index of the corresponding buffer in the list, prefixed with @buffer_reference@.

Making use of the Jupyter serialization protocol in xwidgets

Serialization

The serialization is handled by the free function

xwidgets_serialize(value, patch, buffers);

where

  • the first argument is a const reference to the value,

  • the second argument (patch) to the JSON object being written.

  • the third argument (buffers) is a reference to the sequence of buffers of the message.

picked up by argument-dependent lookup, and apply to all xwidgets properties holding values of that type.

Note

The default implementation of xwidgets_serialize simply invokes the JSON serialization for that type. In most cases, overloading xwidgets_serialize is not necessary. This is mostly relevant for properties for which one wants to bypass JSON serialization or make use of binary serialization.

Deserialization

The deserialization is handled by the free function

set_property_from_patch(property, patch, buffers);

where

  • the first argument is a reference to the property,

  • the second argument (patch) holds a const reference to the JSON patch being read.

  • the third argument (buffers) holds a const reference to the sequence of buffers being read.

set_property_from_patch is called for each property of the widget.

The default behavior of set_property_from_patch is to invoke the JSON deserialization for each property and it can be specialized for a specific property type.

For example, the overload of set_property_from_patch for the value property of the image widget reads:

inline void set_property_from_patch(decltype(image::value)& property,
                                    const nl::json& patch,
                                    const xeus::buffer_sequence& buffers)
{
    auto it = patch.find(property.name());
    if (it != patch.end())
    {
        using value_type = typename decltype(image::value)::value_type;
        std::size_t index = buffer_index(patch[property.name()].template get<std::string>());
        const auto& value_buffer = buffers[index];
        const char* value_buf = value_buffer.data<const char>();
        property = value_type(value_buf, value_buf + value_buffer.size());
    }
}

Note

decltype(image::value) is the type of the value property of the image widget, which is unique to the image widget, (more specifically, its type is an internal class of the image class).

This specialization is a better match than the default one and is picked-up by argument-dependent lookup, however, this will not apply to properties of other widgets or other properties of this widget also holding a std::vector<char>.

Overloading xwidgets_deserialize

The default implementation of set_property_from_patch reads:

template <class P>
inline void set_property_from_patch(P& property,
                                    const nl::json& patch,
                                    const xeus::buffer_sequence& buffers)
{
    auto it = patch.find(property.name());
    if (it != patch.end())
    {
        typename P::value_type value;
        xwidgets_deserialize(value, *it, buffers);
        property = value;
    }
}

which means that the default behavior is to call into xwidgets_deserialize with the value held by the property. A way to specify a deserialization method for a user-defined type is to overload the xwidgets_deserialize method for that type in the same namespace where the type is defined. Then, it will be picked up by argument-dependent lookup, and apply to all xwidgets properties holding values of that type.

Note

The default implementation of xwidgets_deserialize simply invokes the JSON deserialization for that type. In most cases, overloading xwidgets_deserialize or set_property_from_patch is not necessary. This is mostly relevant for properties for which one wants to bypass JSON deserialization or make use of binary deserialization.