Swiftpack.co - outfoxx/PotentCodables as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by outfoxx.
outfoxx/PotentCodables 2.4.2
🧪 PotentCodables - A potent set of implementations and extensions to the Swift Codable system
⭐️ 39
🕓 1 week ago
iOS macOS watchOS tvOS
.package(url: "https://github.com/outfoxx/PotentCodables.git", from: "2.4.2")

🧪 PotentCodables

GitHub Workflow Status Coverage

A potent set of implementations and extension to the Swift Codable system


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


Swift Package Manager

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 implementors
  • PotentJSON provides JSON format support
  • PotentCBOR provide CBOR format support
  • PotentASN1 provides ASN.1 support
  • PotentYAML provides YAML 1.2 support


Using Encoders/Decoders

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)

Provided Formats

  • YAML - YAMLEncoder/YAMLDecoder or YAML.Encoder/YAML.Decoder A conformant YAML 1.2 implementation implemented via libfyaml. Due to the fact that JSON is a subset of YAML 1.2 the decoder can parse and decode YAML & JSON documents.

  • JSON - JSONEncoder/JSONDecoder or JSON.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 using AnyValue.

  • CBOR - CBOREncoder/CBORDecoder or CBOR.Encoder/CBOR.Decoder A conformant implementation of the CBOR serialization format written in pure Swift.

  • ASN.1 - ASN1Encoder/ASN1Decoder or ASN1.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 or AnyValue.Encoder/AnyValue.Decoder An in-memory transcoding implementation for working with unstructured values using AnyValue.

Extended Interfaces

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?

When used on Apple platforms where the Combine framework is available, all the encoders conform to Combine's TopLevelEncoder and all decoders conform to its TopLevelDecoder.

Polymorphic Encoding/Decoding

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.

Type serialization & lookup

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.

Allowed Types in Frameworks

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.

Raw Value Container

Each decoder has an in memory representation known as the "tree" value. The great thing about tree values is that they hold the values in their exact serialized representation. For example, JSON tree values store numbers as a specialized JSON.Number that stores the exact number value as a string along with a number of other properties for helping the conversion of strings to integer or floating point numbers. Accessing this JSON.Number and reading the exact decimal value serialized in JSON is available from tree values.

The decoders support accessing the tree value using specializations of the protocol TreeValueDecodingContainer which extends the SingleValueDecodingContainer protocol.

Decoding JSON values using the TreeValueDecodingContainer as follows:

func init(from decoder: Decoder) throws {
let treeContainer = try decoder.singleValuedContainer() as! TreeValueDecodingContainer
self.jsonValue = try treeContainer.decodeTreeValue() as! JSON

Each tree value has the ability to "unwrap" itself (using it's unwrapped property) into it's the best available standard Swift type, returned as an Any. As an example, unwrappingthe the JSON value 123.456 result in a Swift Double.

Tree values are returned as an Any to allow easy support any possible tree value. For this reason the TreeValueDecodingContainer has a convenience method to access the unwrapped tree value without excessive casting.

Decoding unwrapped JSON values using the TreeValueDecodingContainer as follows:

func init(from decoder: Decoder) throws {
let treeContainer = try decoder.singleValuedContainer() as! TreeValueDecodingContainer
self.value = try treeContainer.decodeUnwrappedValue()

AnyValue - Unstructured Values

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.



Stars: 39
Last commit: 1 week ago
jonrohan Something's broken? Yell at me @ptrpavlik. Praise and feedback (and money) is also welcome.

Release Notes

2.4.2 - Bug Fixes
1 week ago

What's Changed

Full Changelog: https://github.com/outfoxx/PotentCodables/compare/2.4.1...2.4.2

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics