Specifying Additional Type Information
There are some practices that are not strictly necessary to implement. However, they can be beneficial when you design your own types or customize the serialization process. This is especially true when you want to reduce the complexity of the resulting output file and the amount of customization required to serialize your own types.
Additional information for types or properties can be specified via calls to addTypeInformation<T>, similar to registering a markup extension.
This section describes the most common customization options. For the full specification, please refer to the TypeScript module definition file.
String Conversions
String conversions allow data to be converted to and from XML attribute values instead of being written to XML elements. This can significantly reduce the size of the output XML.
A string conversion is defined by a pair of conversion functions (toString
and fromString
) that
can be assigned to the stringConversion
attribute of a type or property information.
String conversions can be specified for entire types or individual properties. If an entire type is annotated, you also need to specify the type of any property that holds objects of the annotated type. This is because, during deserialization, the framework needs to know which conversion function to invoke.
// A very simple custom object which will just be converted from/to its wrapped number value
class CustomObject extends BaseClass() {
value = 0
}
// A custom type that uses the object for a property
class CustomObjectContainer extends BaseClass() {
wrapped = null
}
// Register a GraphML string conversion for CustomObject
graphMLIOHandler.addTypeInformation(CustomObject, {
stringConversion: {
toString(o) {
return o.value.toString()
},
fromString(s) {
const o = new CustomObject()
o.value = s !== null ? parseInt(s) : 0
return o
}
}
})
graphMLIOHandler.addTypeInformation(CustomObjectContainer, {
properties: {
// Inform the framework that the wrapped property has the type CustomObject
// This is required because the framework needs to know into which type
// a string value should be converted.
wrapped: { type: CustomObject }
}
})
// This object tree
const co = new CustomObject()
co.value = 5
const coc = new CustomObjectContainer()
coc.wrapped = co
// will result in the following output:
//
// <asdf:CustomObjectContainer wrapped="5"/>
// A very simple custom object which will just be converted from/to its wrapped number value
class CustomObject extends BaseClass() {
public value = 0
}
// A custom type that uses the object for a property
class CustomObjectContainer extends BaseClass() {
public wrapped: CustomObject | null = null
}
// Register a GraphML string conversion for CustomObject
graphMLIOHandler.addTypeInformation(CustomObject, {
stringConversion: {
toString(o): string | null | undefined {
return (o as CustomObject).value.toString()
},
fromString(s): CustomObject | null | undefined {
const o = new CustomObject()
o.value = s !== null ? parseInt(s) : 0
return o
}
}
})
graphMLIOHandler.addTypeInformation(CustomObjectContainer, {
properties: {
// Inform the framework that the wrapped property has the type CustomObject
// This is required because the framework needs to know into which type
// a string value should be converted.
wrapped: { type: CustomObject }
}
})
// This object tree
const co = new CustomObject()
co.value = 5
const coc = new CustomObjectContainer()
coc.wrapped = co
// will result in the following output:
//
// <asdf:CustomObjectContainer wrapped="5"/>
Content Properties
At most one property of an object can be specified as a contentProperty
. If a
property is designated as the contentProperty
, its value does not need to be enclosed in an explicit XML element.
// The inner object
class CustomObject extends BaseClass() {}
// A custom type that uses the object for a property
class CustomObjectContainer extends BaseClass() {
wrapped = null
}
// Set the content property
graphMLIOHandler.addTypeInformation(CustomObjectContainer, {
contentProperty: 'wrapped'
})
// This object tree
const coc = new CustomObjectContainer()
coc.wrapped = new CustomObject()
// will result in the following output
// (note that the tag for CustomObjectContainer.wrapped is missing)
//
// <asdf:CustomObjectContainer>
// <asdf.CustomObject/>
// </asdf:CustomObjectContainer>
//
// The inner object
class CustomObject extends BaseClass() {}
// A custom type that uses the object for a property
class CustomObjectContainer extends BaseClass() {
public wrapped: CustomObject | null = null
}
// Set the content property
graphMLIOHandler.addTypeInformation(CustomObjectContainer, {
contentProperty: 'wrapped'
})
// This object tree
const coc = new CustomObjectContainer()
coc.wrapped = new CustomObject()
// will result in the following output
// (note that the tag for CustomObjectContainer.wrapped is missing)
//
// <asdf:CustomObjectContainer>
// <asdf.CustomObject/>
// </asdf:CustomObjectContainer>
//
Additional Information for Properties
Individual properties can be annotated by providing the information as part of the
properties
attribute of a type information specification:
// Sample object with two properties
class CustomObject extends BaseClass() {
_p1 = false
_p2 = null
get p1() {
return this._p1
}
set p1(value) {
this._p1 = value
}
get p2() {
return this._p2
}
set p2(value) {
this._p2 = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
p1: {
/* information for p1 */
},
p2: {
/* information for p2 */
}
}
})
// Sample object with two properties
class CustomObject extends BaseClass() {
private _p1 = false
private _p2: INodeStyle | null = null
get p1(): boolean {
return this._p1
}
set p1(value: boolean) {
this._p1 = value
}
get p2(): INodeStyle | null {
return this._p2
}
set p2(value: INodeStyle | null) {
this._p2 = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
p1: {
/* information for p1 */
},
p2: {
/* information for p2 */
}
}
})
Property Type Information
In certain cases, such as string conversions, you need to explicitly
provide information about the type of objects a property can hold.
This is necessary when the type information cannot be automatically determined. You can provide
this information with the type
attribute of a property specification:
// Sample object with two properties
class CustomObject extends BaseClass() {
_p1 = false
_p2 = null
get p1() {
return this._p1
}
set p1(value) {
this._p1 = value
}
get p2() {
return this._p2
}
set p2(value) {
this._p2 = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
p1: { type: Boolean },
p2: { type: INodeStyle }
}
})
// Sample object with two properties
class CustomObject extends BaseClass() {
private _p1 = false
private _p2: INodeStyle | null = null
get p1(): boolean {
return this._p1
}
set p1(value: boolean) {
this._p1 = value
}
get p2(): INodeStyle | null {
return this._p2
}
set p2(value: INodeStyle | null) {
this._p2 = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
p1: { type: Boolean },
p2: { type: INodeStyle }
}
})
Default Values
You can provide a default value for a property with the default
attribute of
a property specification. The framework evaluates this property and
prevents writing properties that have their default value, which can
significantly reduce the size of the output.
Note that a default value specification is only evaluated when writing. To correctly read an object representation where a property value is not specified, you must also initialize the property with a field or property initializer or in the constructor.
// Sample object with a default value
class CustomObject extends BaseClass() {
_vertical = false
get vertical() {
return this._vertical
}
set vertical(value) {
this._vertical = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
vertical: { default: false }
}
})
// This object tree
const coc = new CustomObject()
coc.vertical = false
// will result in the following output
// (note that the "vertical" property is skipped)
//
// <asdf:CustomObject/>
// Sample object with a default value
class CustomObject extends BaseClass() {
private _vertical = false
get vertical(): boolean {
return this._vertical
}
set vertical(value: boolean) {
this._vertical = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
vertical: { default: false }
}
})
// This object tree
const coc = new CustomObject()
coc.vertical = false
// will result in the following output
// (note that the "vertical" property is skipped)
//
// <asdf:CustomObject/>
String Conversions
You can specify string conversions for individual properties, similarly to string conversions for types.
Excluding Properties from Writing
You can explicitly exclude a property from writing by setting its visibility
attribute to hidden
.
Handling Collection Properties
Collection-valued properties are often read-only, meaning the property is initialized by its owner and only provides iterator access. The specific type of such a collection is also often considered an implementation detail.
GraphML supports this scenario by allowing you to specify a property’s visibility
as content
and explicitly
specifying the property type. In this case, the contents of the collection will
be serialized directly as children of the surrounding property element, without
an intermediate container element.
// Sample object with various collection properties
class CustomObject extends BaseClass() {
_readonlyCollection = new List()
_normalCollection = null
get readonlyCollection() {
return this._readonlyCollection
}
get normalCollection() {
return this._normalCollection
}
set normalCollection(value) {
this._normalCollection = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
// Specify content visibility to enable serialization for that property
readonlyCollection: { type: IList, visibility: 'content' },
// Specify no explicit visibility
normalCollection: { type: IList }
}
})
// This object tree
const co = new CustomObject()
co.normalCollection = new List(['one', 'two'])
co.readonlyCollection.push('three')
co.readonlyCollection.push('four')
// will result in the following output
//
// <asdf:CustomObject>
// <asdf:CustomObject.normalCollection>
// <x:List>
// <sys:String>one</sys:String>
// <sys:String>two</sys:String>
// </x:List>
// </asdf:CustomObject.normalCollection>
// <asdf:CustomObject.readonlyCollection>
// <sys:String>three</sys:String>
// <sys:String>four</sys:String>
// </asdf:CustomObject.readonlyCollection>
// </asdf:CustomObject>
//
// Sample object with various collection properties
class CustomObject extends BaseClass() {
private _readonlyCollection: IList<string> = new List<string>()
private _normalCollection: IList<string> | null = null
get readonlyCollection(): IList<string> {
return this._readonlyCollection
}
get normalCollection(): IList<string> | null {
return this._normalCollection
}
set normalCollection(value: IList<string>) {
this._normalCollection = value
}
}
graphMLIOHandler.addTypeInformation(CustomObject, {
properties: {
// Specify content visibility to enable serialization for that property
readonlyCollection: { type: IList, visibility: 'content' },
// Specify no explicit visibility
normalCollection: { type: IList }
}
})
// This object tree
const co = new CustomObject()
co.normalCollection = new List(['one', 'two'])
co.readonlyCollection.push('three')
co.readonlyCollection.push('four')
// will result in the following output
//
// <asdf:CustomObject>
// <asdf:CustomObject.normalCollection>
// <x:List>
// <sys:String>one</sys:String>
// <sys:String>two</sys:String>
// </x:List>
// </asdf:CustomObject.normalCollection>
// <asdf:CustomObject.readonlyCollection>
// <sys:String>three</sys:String>
// <sys:String>four</sys:String>
// </asdf:CustomObject.readonlyCollection>
// </asdf:CustomObject>
//