ObjSer reference implementation in Swift.
Note: Though this library complies with the ObjSer specification, it is in alpha stages and is not recommended for production use as it is largely untested, and the API is subject to breaking changes.
See the ObjSer repository for a description of the serialisation format.
- Serialisation of any Swift type, including custom structs, classes, and enums (protocol conformance required, but provided by the library for most standard library types)
- Serialisation of values & collections of non-concrete type (see Protocol types)
- Serialisation of any object graph, including cyclic graphs (where objects reference each other in a loop)
- Deduplication: objects that are referenced multiple times are only stored once
Carthage is a simple, decentralised dependency manager. Add the following line to your Cartfile:
github "ObjSer/objser-swift" ~> 0.4
After running carthage update
, add the relevant framework (ObjSer iOS or ObjSer Mac) from Carthage/Build to the Embedded Binaries section in your target.
Download or clone the repository, and drag ObjSer.xcodeproj into your project. In your target settings, add the relevant framework (ObjSer iOS or ObjSer Mac) to the Embedded Binaries section in the General tab, and to Target Dependences in the Build Phases tab.
OR
Add all source files in the Objser folder directly to your project. They will be treated as part of your project's module, which may cause namespace conflicts.
The serialiser serialises a single root object, as per the specification.
// the root object to be serialised, for example an array of CGPoint
let rootObject: [CGPoint] = ...
// create an output stream (this API will change in the future)
let stream = OutputStream()
// serialise the object to the output stream
ObjSer.serialise(rootObject, to: stream)
// get the resulting byte array
let bytes = stream.bytes
// create an input stream (this API will change in the future)
let stream = InputStream(bytes: bytes)
// provide the deserialiser with necessary type information by specifying the root object's type
let rootObject = try? ObjSer.deserialiseFrom(stream) as [CGPoint]
To make a type serialisable, add conformance to the Serialisable
protocol or one of its subprotocols.
Conformance extensions for most standard library types are provided.
The Serialisable
protocol is intended for encoding primitive types: integers, strings, arrays, dictionaries, etc. A conforming object must be able to initialise from a Deserialising
value, and produce a Serialising
value representing itself when requested.
A class that may be part of a cycle in the object graph must conform to Serialisable
directly:
class Cyclic: Serialisable {
var a = 0
weak var c: Cyclic!
required init() { }
static func createForDeserialising() -> Serialisable {
return self.init()
}
func deserialiseWith(des: Deserialiser) throws {
a = try des.deserialiseKey("a") ?? 0
c = try des.deserialiseKey("c")
}
func serialiseWith(ser: Serialiser) {
ser.serialise(a, forKey: "a")
ser.serialise(c, forKey: "c")
}
}
A static constructor is required in place of an initialiser in order to make it possible to extend non-final classes to conform to Serialisable
(it is impossible to add required initialisers in extensions).
An empty initialiser separated from the deserialisation process is used in order to correctly reconstruct cycles in the object graph if necessary.
If you are extending a non-final class out of your control that never forms cycles, e.g. NSData
, and are not able to add required initialisers, conform to AcyclicSerialisable
:
extension NSData: AcyclicSerialisable {
public static func createByDeserialisingWith(des: Deserialiser) throws -> AcyclicSerialisable {
let bytes = try des.deserialiseData()
return bytes.withUnsafeBufferPointer { buf in
return self.init(bytes: buf.baseAddress, length: bytes.count)
}
}
public func serialiseWith(ser: Serialiser) {
var bytes = ByteArray(count: length, repeatedValue: 0)
bytes.withUnsafeMutableBufferPointer { (inout buf: UnsafeMutableBufferPointer<Byte>) in
self.getBytes(buf.baseAddress, length: length)
}
ser.serialise(data: bytes)
}
}
This protocol is provided as a convenience for types that match the criteria for AcyclicSerialisable
, and are able to implement required initialisers.
extension Bool: InitableSerialisable {
public init(deserialiser des: Deserialiser) throws {
self = try des.deserialiseBool()
}
public func serialiseWith(ser: Serialiser) {
ser.serialise(boolean: self)
}
}
See Conformance.swift for further examples.
Note: Do not catch errors thrown by Deserialiser
's deserialise
functions. If deserialisation fails, rethrow a caught error, or throw a new one.
To serialise an object of protocol type, the object must be saved along with a unique type identifier so it can be correctly deserialised. Override the static variable typeUniqueIdentifier
in each concrete type you plan to serialise when stored in a variable of protocol type, to return a unique value:
extension Int {
static var typeUniqueIdentifier: String? {
return "Int"
}
}
To successfully deserialise a collection of protocol type, pass an array of the types that may occur in the collection to the deserialiser:
let array: [Serialisable] = try ObjSer.deserialiseFrom(stream, identifiableTypes: [Int.self, Float.self])
Note: typeUniqueIdentifier
is defined in the Serialisable
protocol. All protocol types must conform to the Serialisable
protocol in order to be serialised.
-
The errors thrown by various functions are currently largely undocumented, and their associated objects are not very useful for determining the cause of an error. These will be significantly changed, and should not be depended on (use
try?
on throwing functions instead trying to make sense of a caught error). -
The current
InputStream
andOutputStream
structs are temporary; a better IO API will be added in the future. -
Due to the inability to add constrained protocol inheritance (
extension Array : Serialisable where Element : Serialisable
) in Swift 2.1, extensions toArray
,Dictionary
mark the entire type as conforming, and raise runtime errors if a non-conforming type is contained within.An unfortunate side effect of this is the lack of compile-time errors when archiving or mapping an array, dictionary, or optional containing value(s) that do not conform to
Serialisable
. This is eased somewhat by descriptive runtime error messages that provide sufficient type information to locate and correct non-conforming types.The alternative to this approach would be to provide a large number of boilerplate methods in the serialiser and deserialiser to handle various permutations of nested arrays, dictionaries, optionals, etc., which would provide compile-time type-checking at the expense of API simplicity, code size, and the inevitable lack of support for an obscure edge case.
In future, it will hopefully be possible to amend this as complete generics are added to Swift 3.0.