[FR] toString() method for juce::ReferenceCountedObject

We often use juce::ReferenceCountedObject wrappers to allow custom types to be stored as properties in a juce::ValueTree.

However when we then want to parse to/from an XML file, this assertion in juce::NamedValueSet prevents that as it doesn’t know how to turn the object into a string:

A virtual toString() method on juce::ReferenceCountedObject would allow us to define how we want our custom type to be serialised. Deserialisation can then be handled manually to turn a string vars into our custom types again.

Why not use a variant converter for custom types?

This is the compromise we typically use, however this can be really costly for more complex types as we constantly have to convert back and forth between our custom types and juce::var. I.e. anytime the app’s state is updated, we have to convert the object to a juce::var and put it in a VT, and anytime the state needs to be read, we have to convert that juce::var back to a new instance of the expensive-to-construct object.

Being able to use juce::ReferenceCountedObject would mean we can just construct the objects once and freely pass them around, and the toString() method would allow us to properly serialise these objects too.

Tangential, but why can’t arrays be written to XML?

jassert (! i.value.isArray());

Couldn’t this simply be replaced with juce::JSON::toString()? Parsing back again could just check if the attribute’s value starts and ends with []

I assume you mean that the result of juce::JSON::toString() would be stored as the value of the attribute. So effectively we would be storing JSON data in an XML. I think the same argument could be applied an Object too? An array could be an array containing all sorts of data no? I could be wrong but it seems like that would have some edge cases to consider, for example if you have a bunch of strings in JSON they will be double quoted which would cause issues for XML so they would need encoding / decoding.

Generally to store an array in XML it would be something more like

<someArray>
    <value>1</value>
    <value>2</value>
    <value>3</value>
</someArray>

I haven’t spent time looking at it but I’m not sure how easy it would be to do this without causing some other issues/edge case.

We’ll need to give it some thought but I’m not immediately sold by a virtual method on ReferenceCountedObject, maybe there is something else we could do.

A simple example to work with might be helpful, maybe use a sleep or something to highlight some kind of expensive conversion that is avoided by this suggestion?

Something like a variant converter could work to avoid changing existing types:

namespace juce
{
  template <typename MyRCO>
  struct ReferenceCountedObjectSerialiser
  {
    static juce::String toString(const MyRCO& myObject) {}
  };
}

juce::VariantConverter constructs a new instance of your custom type when you call fromVar() - so for a type with a lot of data, e.g. struct Foo { int data[10000000]; }, using a variant converter is really inefficient when you regularly need to convert to-from juce::var.

Wrapping such an object in a ReferenceCountedObject wrapper (or making the object itself a RCO) means you just construct it once - but doing so prevents it being serialised.

I guess we can compromise and pre-serialise those properties by making a copy of the VT and converting them to strings before writing to XML, but for large trees that would be really inefficient.

Yeah so for a VT like:

juce::ValueTree {
  "State",
  {
    {"some-data", juce::Array<juce::var>{ 1, 2, 3, 4 }},
  },
};

I’d expect an XML file like:

<State some-data="[1, 2, 3, 4]">
</State>

I agree that mixing JSON and XML is a bit weird (I’d settle for a JSON equivalent of ValueTree? :wink: ), but since all XML attributes should be strings, and all JSON objects can be represented as strings, all JSON objects are valid XML attributes. And juce::var is intended as a C++ representation of a JSON object/value, so I’d expect an array var to be convertible to/from an XML attribute.

Sure, but that would be a separate VT layout, no? If that’s what I wanted my XML to look like, I would construct my VT like:

juce::ValueTree {
  "State",
  {},
  {
    juce::ValueTree {
      "someArray",
      {},
      // etc..
    },
  },
};

I think that would get my vote too, but I’ve honestly not thought about it enough, at least not recently enough to have considered if there is some issue I’m not considering.

It seems like they are, but it’s pretty ugly! For example

    const auto array = juce::var { juce::StringArray { "one", "two", "three" } };
    auto xml = juce::XmlElement ("xml");
    xml.setAttribute ("array", juce::JSON::toString (array, true));
    DBG (xml.toString());

becomes

<xml array="[&quot;one&quot;, &quot;two&quot;, &quot;three&quot;]"/>

Feels like it’s loosing its human readable appeal at that point. I imagine it could get a whole lot messier than that too.

Coming back the other way from XML though what if someone had a string stored that was a valid array? I don’t think there would be any way to know it was actually just a string? so however unlikely, maybe it could break existing code?

Anyway that feels beside the point, I’ll give some thought to the ReferenceCountedObject issue.

1 Like

Maybe you want a generic overridable toXmlElement() instead ? That could make it work when you serialize a ValueTree to xml with an object, but won’t work for binary value trees.