This a module to provide XML object serialization in Nodejs. It is meant to be at least somewhat compatible with XML from/to the C# XmlSerializer (and DataContractSerializer with a little work).
This module has no dependencies (except for one of the supported XML libraries). Other heavier packages might be created that import this module and other dependencies for those who want a particular feature set out-of-the-box.
Motivation
The original author could not find an XML serializer for Nodejs that just took class instances to XML and back, so this meager one was started. Links to alternative or derivative libraries may be added to this readme section over time to help others searching for similar solutions.
- xml-csharp-cereal - This one.
- xml-decorators - Interesting typescript module using decorators rather than templates. Currently only serializes to XML, but that could change.
Installation
This module is contained in a single file, so you can either just grab xml-csharp-cereal.js and add it to your project directly, or install it from npm.
npm install xml-csharp-cereal
XML Libraries
This module is not written to depend on a singular XML library. Therefore you need to install a supported XML package separately. Each supported XML library would have an associated "to_XmlLib()" and "from_XmlLib()" on XmlTemplateFactory.
- xml2js { test/test_xml2js.js } - Supports all current features.
- xmldom { test/test_xmldom.js } - Supports all current features. (To use an xmldom clone/fork, see options to specify an alternate DOMImplementation on which to_xmldom can call createDocument.)
Support for more XML libraries might be added over time.
Code Example
JSDoc API documentation is available in API_JSDOC.md.
// Importing the moduleconst xml = ;
Deserializing from XML
Assume we have an XML file to deserialize into "my_obj", which if successful should be an actual instanceof 'MyClass1'.
var factory = MyClass1 MyClass2;var xml_options = {}; // see section on options // Example using xml2js to parse XML file read by fsfs;
Serializing to XML
Assume we have "my_obj" which is instanceof MyClass1 and we want to serialize to an XML file.
var factory = MyClass1 MyClass2;var xml_options = {}; // see section on options // Example using xml2js to build XML file written by fsvar xml_obj = factory; // serialize my_objvar builder = ;var xml_data = builder;fs;
Setting Up Your Classes
In order to serialize or deserialize your classes, this library needs an XmlTemplate for each class describing the properties to include. This can be done by defining a static getXmlTemplate() on the class itself, or via some external means like a separate function.
{ thisMyString = null; // string thisMyNumber = 0; // number thisSomeClass = null; // instance of MyClass2 } static // you could use built-in template var temp = this; temp; // like a C# string temp; // like a C# int temp; // another class return temp; // Or you could generate the template separately { thisOtherString = null }{ var temp = MyClass2; temp; return temp;}
Setting Up Your XmlTemplateFactory
An XML file may have various classes and types in it. An XmlTemplateFactory stores everything needed to decode or encode them.
// Constructor takes multiple XmlTemplate's and/or classes with getXmlTemplate()var factory = MyClass1 ; // You can also add templates to an existing instancevar factory = ;factory;factory;
Decoders and encoders for some common simple types are stored in XmlTemplateFactory by default. You can add or override these per instance.
// Defining a simple type called 'hex'// where factory = new XmlTemplateFactory(...)factory;{ // parse hex in XML node to number in object property var ret = ; if !Number throw 'Decoder for "hex" cannot parse node into finite number'; return ret;}{ // write hex into XML node from object property value val = ; // make sure val is actual number if !Number throw 'Encoder for "hex" requires finite number'; return val;} // when building XML template...temp; // this.MyHex is number stored as hex
64 bit integers are handled as strings by default. However if you are using a library such as long.js, you can override the simple type decoder/encoder to utilize a 64 bit number class.
const Long = ;{ // parse XML value into Long instance if val == null return null; // handle nullable return Long; // let it throw on error}{ // output Long as string for XML node if val == null return null; // handle nullable // if not Long instance already, try to parse into a Long val = Long; return val; // let it throw on error}{ // parse XML value into Long instance if val == null return null; // handle nullable return Long; // let it throw on error}{ // output Long as string for XML node if val == null return null; // handle nullable // if not Long instance already, try to parse into a Long val = Long; return val; // let it throw on error}// Overriding default int64 handlers// where factory = new XmlTemplateFactory(...)factory;factory;
The default DateTime decoder/encoder uses ISO string and javascript Date object. The default for TimeSpan decodes ISO string to seconds using the moment.js regex method, as there is no built-in javascript equivalent. Both DateTime and TimeSpan decode/encode can be overridden to use other methods or libraries, such as moment.js or TimeSpan.js.
XmlTemplate - Add Methods
The XmlTemplate class provides add functions for the all of the XmlTemplateFactory's built-in types.
Method | Description |
---|---|
add(prop_name, class_name, arr_levels, arr_namespace, isNullable, hasExplicitTypeTag, isFlatArray) | Add instance of class (or simple type) with given array levels and options. |
addString(prop_name, ...) | Same as add(prop_name, 'string', ...) |
addBool(prop_name, ...) | Same as add(prop_name, 'bool', ...) |
addInt(prop_name, ...) | Same as add(prop_name, 'int', ...) |
addFloat(prop_name, ...) | Same as add(prop_name, 'float', ...) |
addDouble(prop_name, ...) | Same as add(prop_name, 'double', ...) |
Also included: addInt16, addUInt16, addInt32, addUInt32, addInt64, addUInt64, addSByte, addByte, addUInt, addShort, addUShort, addDateTime, and addTimeSpan.
XmlTemplate - Add Array
Just leverage the optional arr_levels parameter of add functions. Pass a number of dimensions or an array of level names to use for XML tags.
// int[] MyIntArraytemp; // int[][] MyJagIntArray using implicit level namestemp;// int[][] MyJagIntArray using explicit level namestemp;
NOTE: During testing a strange behavior was observed with C# XmlSerializer where defining a nullable integer array "int?[]" before a jagged integar array "int[][]" would cause the tag names in int[][] to change from "ArrayOfInt" to "ArrayOfInt1". This can be mitigated by either declaring the nullable array last or adding an explicit XmlArrayItem attribute to the subsequent int[][].
[XmlArrayItem(ElementName = "ArrayOfInt", IsNullable = false, Type = typeof(int[]))]public int[][] MyJagIntArray;
XmlTemplate - Add Flat Array
A last minute addition was made to handle flat/headless/rootless XML arrays. By default array element tags are under a single tag, but you can mark the property as having a flat array and the array elements will simply be listed directly in the containing class.
// C# declaration of flat XML array prop using element tag names 'Color'[XmlElement(ElementName = "Color")]public string[] Hues;
// JS adding prop as flat array with one dimension level name of 'Color'temp; // OR the increasing amount of XmlTemplate.add() / XmlTemplateItem constructor argstemp;
. . .<!-- 'Hues' prop is flat (elements at same level as props) -->redgreenblue. . .<!-- vs. 'MyEnumArray' prop with default structure --> one two. . .
XmlTemplate - Add Nullable
Just call nullable() on an XmlTemplateItem to make it nullable.
// public int? MyNullInt;temp;// public int?[] MyNullIntArr;temp; // OR use isNullable parameter on addtemp;temp;
XmlTemplate - Add as XML Attribute
Just call attr() on an XmlTemplateItem to use it as XML attribute. Try to keep to simple types as XML attributes.
// C# declaration[XmlAttribute]public string SomeAttr;
// Building XmlTemplate in JStemp;
XmlTemplateFactory - Add Enum
There are three ways to decode/encode an enumeration type/class.
- Add a simple object representation of the enum to factory.
var MyEnumSimple = zero:0 one:1 two:2 three:3 ;factory;
- Define a simple type decoder and encoder on the factory.
factory;{ var lut = zero:0 one:1 two:2 three:3 if lutval==undefined throw 'MyEnum does not define ' + val; return lutval;}{ var lut = 0:'zero' 1:'one' 2:'two' 3:'three' if lutval==undefined throw 'MyEnum does not define ' + val; return lutval;}
- Add object or class that defines getEnumValue(name) and getEnumName(value) functions for the factory to use.
factory;const MyEnumExplicit = { var lut = zero:0 one:1 two:2 three:3 ; return lutn; } { var lut = 0:'zero' 1:'one' 2:'two' 3:'three' return lutv; }
XmlTemplateFactory - Explicit Dictionary
// Brief example of what an explicit dictionary might look like { thisKey=k; thisValue=v; } static var temp = this; temp
XmlTemplateFactory - Add Object as Implicit Dictionary
One can certainly construct an explicit dictionary class with explicit templates, but many may opt to use an object whose enumerable property names are used as dictionary keys. (At this time keys should probably be a simple type when using implicit dictionary.) Basically you need to register a class with the factory that spells out the key-value pair tag name and property info for the key and value.
// longest formfactory; // long form with array constructor shortcutsfactory; // short form assumes names and keys of stringfactory;
Once a dictionary class is registered to the factory, you can use the class name when creating a template.
// property using a simple class acting as dictionary// C# equivalent: SerializableDictionary<string, int> MyIntDict;thisMyIntDict = "dogs":3 "cats":2 ; // added to template with class name of dictionarytemp;
XmlTemplateFactory - Dictionaries with Type Tags
Some implementations of serializable dictionary out there use explicit type tags inside the key and value tags.
third 3
Just set the 'hasExplicitTypeTags' parameter of addDict().
XmlTemplate - Constructor Arguments
By default, an XmlTemplate generates new instances of its assigned class without constructor arguments. However the constructor for XmlTemplate takes an optional second parameter, which is an array of arguments to use when constructing new instances. This is primarily used internally, but if it is useful to you in some way, it is there.
// XmlTemplate(parent_class, constructor_args)var args = KeyName key ValueName value;var temp = KeyValuePairStub args;// Below statement is equivalent to// "var obj = new KeyValuePairStub(...args)"var obj = temp;
Derived Classes
See test/Test2.js and test/Test3.js for examples. Basically use XmlTemplate.extend() on a copy of base class's XmlTemplate and add the derived properties to it.
{ super; thisSuperName = null; } static var temp = super; temp; temp; return temp;
DataContract / DataContractSerializer
Some observations of DataContract XMLs:
- Most everything except built-in types have namespaces.
- Some tag names include a hash suffix derived from namespace info.
- All property tags are in alphabetical order AND derived class props are listed after base props.
- Arrays and dictionaries are tagged with special namespaces.
- Array namespace can vary based on content.
- Jagged array and dictionary type tags follow different naming convention compared to XmlSerializer.
- Built-in DateTime and TimeSpan support using ISO strings.
See test/Test1.js, test/Test3.js, and test/Test4.js for examples. There are some helper methods provided. You might also want to set XmlMode in the options object, particularly if using automatic names in jagged arrays.
// Use XmlMode option to indicate we want DataContract styleconst xml = ;var xml_options = XmlMode: xmlxmlModesDataContractSerializer;var my_obj = factory; // XmlTemplate.setXmlNameSpace() is provided to set a class's XML namespacetemp; // XmlTemplateFactory.applyDataContractNameSpaces() attempts to assign XML namespaces where they are not already defined by the user. You pass the default namespace and it tries to figure things out for you.factory; // XmlTemplate.add() can take an array XML namespace, if you want to set that manually.temp;// XmlTemplateFactory addEnum and addDict also have optional namespace parameters for manually assigning XML namespace.
Serialization Options
The to/from methods of factory can take an options object.
Option | Default | Description |
---|---|---|
XmlMode | xmlModes.XmlSerializer | Value from 'xmlModes' used when difference of behavior is needed, such as XmlSerializer jagged array names vs DataContractSerializer names. |
UseNil | false | If true, instead of omitting null nodes use the nil attribute. |
DOMImplementation | null | Allows you to override the DOM implementation object that to_xmldom will use to call the createDocument function. If null, it will check for 'document.implementation' then try 'xmldom' module. |
Package Scripts
Script | Description |
---|---|
npm run test | Run the tests |
npm run browser | Generate browser versions in 'browser' folder |
npm run doc | Generates API_JSDOC.md |
npm run build | Runs 'browser' then 'test' then 'doc' |
Tests
The tests consist of a node portion (test.js) and a C# portion (csharpxml). csharpxml generates test XMLs for node to read, then node loads/resaves the XML, and csharpxml verifies the result using Compare-Net-Objects. If running on Linux, you will need to install the mono-complete package to run the tests. A pre-compiled version of the csharpxml app is included with the code in the repo, so re-compilation is not needed unless you need to alter that portion of the tests. (It also saves on Travis test time and complexity.)
See the readme in the test folder for more info.
License
The authors and contributors assume no liability or warranty. Use at your own risk. This project can be used under Unlicense (public domain) or Apache-2.0, whichever works for you.
SPDX: (Unlicense OR Apache-2.0)
Can I use it in the Browser?
This is primarily a Nodejs module and that is what npm presently deploys. However the 'browser' folder in the Git repo should contain experimental versions generated from the original js via metascript, which you should be able to use with to_xmldom() and from_xmldom() inside a browser. See the respective test pages in that folder for usage examples as both a classic script and as an ES6 module.
Future?
Things that might could be done in future?
- Make testing more granular?
- Make testing more thorough?
- Add tests for error reporting?
- Improve error reporting?
- Create another package that includes preconfigured extras like Long.js and TimeSpan.js?
- Better In-browser compatibility or deployment?
- Support other odd ball XmlSerializer constructs?