Wednesday, February 4, 2015

Rails ActiveModel/ActiveRecord XML/JSON serialization replacement

I need a bit more control over the XML schema than the standard ActiveRecord/ActiveModel XML serializers provide, and a bit of control over root tags and attribute names in JSON that normally isn't there. I don't need the full control that something like RABL provides, and I'd like deserialization from XML to be automatically available for any XML I can serialize to. None of the existing gems provides what I want without having to hand-code methods on all classes involved, especially the deserialization part. And if I've written the deserialization code and have the hashes to control that, the serialization code based on those same hashes won't be that much additional work. The hard part will be eventually making it a drop-in replacement for the ActiveModel XML and JSON serializers and compatible with the parts of ActiveRecord that interact with serialization. I'll deal with that later, though, I'm just going to lay the groundwork for compatibility now and deal with actually integrating it once I've got the code working outside that framework.

XML serialization/deserialization description and control data:

Item hash:
  • name: element or attribute name
  • source: object attribute name, literal data string, nil for no contents
  • +type: Ruby class name, Boolean short for "TrueClass or FalseClass", Literal for a literal element (no source attribute), default String if this is not the root element or the class being deserialized for the root element
  • options: {options hash}, default {}
  • *attributes: [array of item hashes describing attributes of this element], default []
  • *children: [array of item hashes describing child elements of this element], default []
+ = needed only during deserialization
* = valid only in element item hashes, ignored in attribute item hashes

Options hash:
  • *default: default value if source's value is nil, default no value
  • trim: true/false, trim leading/trailing whitespace, default false
  • elide: true/false, omit if value (after trimming if relevant) is nil or empty, default false
* = not applicable to items of Literal type or items where source is nil.

Control attributes on the object:

xml_attributes: an item hash describing the root element of the object
  • Canonically the source would be nil, but a source attribute is legal if the root element will have actual text content.
  • The type attribute of the root element defaults to the type of the object if not specified, and canonically it isn't specified since forcing a mismatching type will cause problems.
  • The name attribute can be overridden by specifying an element name when serializing the object. When deserializing the root element's name is ignored and it's assumed the caller is deserializing the correct class.
attributes: a hash describing the attributes to be serialized
  • The key is the source attribute name.
  • The value is nil or a string giving the element name to be used.
If xml_attributes is not present, attributes will be used instead. One or the other must be provided.

When serializing XML:
  • If attributes is used then the attributes are serialized as child elements of the root and the default for include_types is true and use_source_names is true if the value is nil and false if the value names an element name. If xml_attributes is used the default for include_types and use_source_names is false. This allows the XML to be unchanged if an old-style attributes hash is being used.
When serializing JSON:
  • If xml_attributes is used then the attributes sub-hash is serialized first followed by the children sub-hash. Items with a nil source or Literal for type are ignored, they're relevant only to XML serialization.
  • When deserializing, if type is absent then the type is determined by Ruby's default rules and the format of the value. Contained objects will end up deserialized to a hash and a class-specific attributes= method will be needed to recognize the attribute name, create an object of the correct class and initialize it with the hash before assigning it to the attribute.
to_xml options:
  • root_name: name of the root element if not the class name of the object
  • include_types: include type and nil attributes in XML
  • use_source_names: follow Ruby's naming conventions for element names
root_name is normally used when attributes is used to control serialization, or if the root element name in xml_attributes needs to be overridden. Setting both include_types and use_source_names to true will yield the same XML normally produced by the old serializer when using xml_attributes .

to_json options:
  • include_root_in_json: include the class name as the root element of the JSON representation, default false
  • include_all_roots_in_json: include the class name as the root element for all contained objects (implies include_root_in_json), default false
include_root_in_json only puts the root element in at the top level, not on contained objects. That makes it compatible with the JSON expected when the matching flag is set in the from_json call.

No comments: