The framework aims to solve three major pain points experienced with Swift's Codable
system:
- Allow decoding and/or encoding values of unknown structure (e.g. any encoded value)
- Support polymorphic type encoding/decoding while still allowing Swift to implement
Codable
- Reduce the complexity and amount of code required to implement and test new serialization formats
- Provide a library of fully featured implementations of popular serialization formats
PotentCodables currently supports Swift Package Manager for project integration. Add a package dependency similar to the following:
.package(url: "https://github.com/outfoxx/PotentCodables.git", from: "1.0.0")
The package provides multiple libraries corresponding to the core library and each format that is provided:
PotentCodables
is the core library to be used by format implementorsPotentJSON
provides JSON format supportPotentCBOR
provide CBOR format supportPotentASN1
provides ASN.1 support
If your only goal is to use one of the provided implementations of a serialization format, not much information is needed beyond the
name of the encoder/decoder pair that you are seeking to use. All of the implementations provided by the package are 100% compatible with
Swift's Codable
system and they all intentionally mimic the interface of Swift's native encoders & Decoder
(e.g. Foundation.JSONEncoder
and Foundation.JSONDecoder
).
For example encoding to CBOR is essentially the same as encoding with Swift's standard JSONEncoder
let data = try CBOREncoder.default.encode(myValue)
- JSON -
JSONEncoder
/JSONDecoder
orJSON.Encoder
/JSON.Decoder
A conformant JSON implementation that is a drop-in replacement for Swift's JSON encoder and decoder provided by Foundation. These implementations offer enhancements to what items can be encoded to (e.g. to/from Strings and to/from native value trees) and offer performance enhancements when usingAnyValue
. - CBOR -
CBOREncoder
/CBORDecoder
orCBOR.Encoder
/CBOR.Decoder
A conformant implementation of the CBOR serialization format written in pure Swift. - ASN.1 -
ASN1Encoder
/ASN1Decoder
orASN1.Encoder
/ASN1.Decoder
A conformant implementation of the ASN.1 serialization format written in pure Swift. ASN.1's position based format can be very ambiguous, even so it is commonly used in situations that require absolute unambiguity. To overcome these issues the ASN1Encoder and ASN1Decoder require a schema be passed to their initializer that directs the encoding and/or decoding. More information is available here - AnyValue -
AnyValueEncoder
/AnyValueDecoder
orAnyValue.Encoder
/AnyValue.Decoder
An in-memory transcoding implementation for working with unstructured values usingAnyValue
.
All provided encoders and decoders come with an extended set of methods that allow different targets and sources when encoding and decoding.
Encoders provide these methods
// Encoding to value tree - Supported by all encoders
func encodeTree<T: Encodable>(_ value: T) throws -> Value
// Encoding to data - supported by text & binary format encoders
func encode<T: Encodable>(_ value: T) throws -> Data
// Encoding to string - supported by text format encoders
func encode<T: Encodable>(_ value: T) throws -> String
Decoders provide these methods
// Decoding from a value tree - supported by all decoders
func decodeTree<T: Decodable>(_ type: T.Type, from value: Value) throws -> T
func decodeTreeIfPresent<T: Decodable>(_ type: T.Type, from value: Value) throws -> T?
// Decoding from data - supported by text & binary format decoders
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
func decodeIfPresent<T: Decodable>(_ type: T.Type, from data: Data) throws -> T?
// Decoding from string - supported by text format encoders
func decode<T: Decodable>(_ type: T.Type, from data: String) throws -> T
func decodeIfPresent<T: Decodable>(_ type: T.Type, from data: String) throws -> T?
Codable
encoders and decoders are a great tool and very convenient. Unfortunately using them with polymorphic types is cumbersome,
to say the least, and near impossible in a lot of other cases.
PotentCodables provides Ref
and EmbeddedRef
to make encoding/decoding polymorphic types very easy. The reference wrappers are
designed to work with concrete type and protcols alike.
Ref
is used to decode values that are "wrapped" with a type name. For example, given JSON similar to the following:
{ "@type" : "MyApp.Foo", "value" : { "name" : "A Value" } }
Ref
can be used to decode a value with little extra code
protocol FooBar {
var name: String { get }
}
struct Foo: FooBar, Codable {
let name: String
}
struct Bar: FooBar, Codable {
let count: Int
}
DefaultTypeIndex.setAllowedTypes([Foo.self, Bar.self]) // Authorize & map allowed types for polymorphic decoding
let val = try JSONDecoder.default.decode(Ref.self).as(FooBar.self) // Decode ref and use the `as` utility to cast it or throw
To encode a value with the required structure and inserting the Swift type name simply use Ref.Value
during encoding:
let data = try JSONEncoder.default.encode(Ref.Value(val))
EmbeddedRef
, which includes EmbeddedRef.Value
, is also provided and is used the exact same way as Ref
. The difference is that
EmbeddedRef
embeds the type name along side the encoded values other keys. For example, the example JSON above would resemble the
following with the key embedded:
{ "@type" : "MyApp.Foo", "name" : "A Value" }
EmbeddedRef
requires the value it encodes to use a keyed-container. Unkeyed and single-value containers cannot be used with
it, but they can be used with Ref
.
The documentation for Ref
and EmbeddedRef
provide a lot of details on their usage as well as documentation of how to customize the
keys used during encoding/decoding.
By default the type serialization & lookup mechanism (see Ref
& DefaultTypeIndex
code documentation) disallows all types to be decoded.
This is to ensure that decoding is secure as only specific/authorized types can be decoded. Additionally, the index mechanism, by default,
uses a type id that does not include the Swift module name so as to ensure stable type ids across modules, frameworks, and languages.
The means that you must explicity map allowed classes prior to using polymorphic decoding. This is done as simply as:
DefaultTypeIndex.setAllowedTypes([Foo.self, Bar.self])
This generates a type id for each type (Foo
& Bar
) and upates the map to allow only those types provided. Note that each call to setAllowedTypes
overwrites the current set of allowed types and as such applications should register them in a single place.
Alternatively you can implement and provide a custom type index (see Ref
, CustomRef
& TypeIndex
code documentation). If you have
an alternate means of looking up types.
The default type index is designed to be convenient and safe for simple applications. Unfortunately this means frameworks must use a custom type index to ensure the types it expects are registered and reduce the chance of inadvertantly creating security vulnerabilities.
Sometimes it is necessary to decode values of any type or that can take on any structure; unfortunately Swift's Codable
is not well suited
for this purpose. PotentCodables provides AnyValue
to fill the gap.
Using AnyValue
is simple, just use it wherever you would normally use an Any
. Since AnyValue
supports Codable
everything else
works as normal including Swift's automatic codable generation.
struct Account : Codable {
let name: String
let data: AnyValue // `data` can store and scalar or complex value
let dataDict: [String: AnyValue] // `dataDict` is required to be a dictionary of name to any values
let dataArray: [AnyValue] // `dataArray` is required to be an array of any values
}
The example Account
struct above has a data
property that can take on any value supported by the codable system. For example
when decoding from JSON, any value or tree of values (including null, bool, string, number, arrray or object) could be saved in
the data
property. Encoding the same Account
value back to JSON will produce equivalent serialized JSON regardless of the contents
of the data
field.
AnyValue
has lots of features to make building and using them natural in Swift, like "dynamic member lookup" to access fields of a
AnyValue.dictionary
. See the documentation for complete details.
Performance
Although AnyValue
is compatible with any conformant Codable
encoder or decoder, PotentCodables decoders specifically have shortcuts
to decode the proper values in a more performant fashion and should be used when possible.