Custom Swift Encoder/Decoder for the Strings Resource Format

Custom Swift Encoder/Decoder for the Strings Resource Format

A bit late to the party here but I feel this might be helpful/informative to others given the question high vote count. (But I won't really get into the actual usefulness of such code in practice—please check the comments above for that.)

Unfortunately, given the coding stack flexibility and type-safeness, implementing a new encoding and decoding solution, for an alternative external representation, is far from a trivial task... so let's get started:

Encoding

Let's start by implementing the encoding part for the desired strings file external representation. (The necessary types will be introduced in a top-down approach.)

Like the standard JSONEncoder class we need to introduce a class to expose/drive our new encoding API. Let's call that StringsEncoder:

/// An object that encodes instances of a data type 
/// as strings following the simple strings file format.
public class StringsEncoder {

/// Returns a strings file-encoded representation of the specified value.
public func encode<T: Encodable>(_ value: T) throws -> String {
let stringsEncoding = StringsEncoding()
try value.encode(to: stringsEncoding)
return dotStringsFormat(from: stringsEncoding.data.strings)
}

private func dotStringsFormat(from strings: [String: String]) -> String {
var dotStrings = strings.map { "\"\($0)\" = \"\($1)\";" }
dotStrings.sort()
dotStrings.insert("/* Generated by StringsEncoder */", at: 0)
return dotStrings.joined(separator: "\n")
}
}

Next, we need to provide a type (e.g., a struct) conforming to the core Encoder protocol:

fileprivate struct StringsEncoding: Encoder {

/// Stores the actual strings file data during encoding.
fileprivate final class Data {
private(set) var strings: [String: String] = [:]

func encode(key codingKey: [CodingKey], value: String) {
let key = codingKey.map { $0.stringValue }.joined(separator: ".")
strings[key] = value
}
}

fileprivate var data: Data

init(to encodedData: Data = Data()) {
self.data = encodedData
}

var codingPath: [CodingKey] = []

let userInfo: [CodingUserInfoKey : Any] = [:]

func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
var container = StringsKeyedEncoding<Key>(to: data)
container.codingPath = codingPath
return KeyedEncodingContainer(container)
}

func unkeyedContainer() -> UnkeyedEncodingContainer {
var container = StringsUnkeyedEncoding(to: data)
container.codingPath = codingPath
return container
}

func singleValueContainer() -> SingleValueEncodingContainer {
var container = StringsSingleValueEncoding(to: data)
container.codingPath = codingPath
return container
}
}

Finally, we need to handle all 3 encoding containers types:

  • KeyedEncodingContainer
  • UnkeyedEncodingContainer
  • SingleValueEncodingContainer
fileprivate struct StringsKeyedEncoding<Key: CodingKey>: KeyedEncodingContainerProtocol {

private let data: StringsEncoding.Data

init(to data: StringsEncoding.Data) {
self.data = data
}

var codingPath: [CodingKey] = []

mutating func encodeNil(forKey key: Key) throws {
data.encode(key: codingPath + [key], value: "nil")
}

mutating func encode(_ value: Bool, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: String, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value)
}

mutating func encode(_ value: Double, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: Float, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: Int, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: Int8, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: Int16, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: Int32, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: Int64, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: UInt, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: UInt8, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: UInt16, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: UInt32, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode(_ value: UInt64, forKey key: Key) throws {
data.encode(key: codingPath + [key], value: value.description)
}

mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath.append(key)
try value.encode(to: stringsEncoding)
}

mutating func nestedContainer<NestedKey: CodingKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
var container = StringsKeyedEncoding<NestedKey>(to: data)
container.codingPath = codingPath + [key]
return KeyedEncodingContainer(container)
}

mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
var container = StringsUnkeyedEncoding(to: data)
container.codingPath = codingPath + [key]
return container
}

mutating func superEncoder() -> Encoder {
let superKey = Key(stringValue: "super")!
return superEncoder(forKey: superKey)
}

mutating func superEncoder(forKey key: Key) -> Encoder {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath = codingPath + [key]
return stringsEncoding
}
}
fileprivate struct StringsUnkeyedEncoding: UnkeyedEncodingContainer {

private let data: StringsEncoding.Data

init(to data: StringsEncoding.Data) {
self.data = data
}

var codingPath: [CodingKey] = []

private(set) var count: Int = 0

private mutating func nextIndexedKey() -> CodingKey {
let nextCodingKey = IndexedCodingKey(intValue: count)!
count += 1
return nextCodingKey
}

private struct IndexedCodingKey: CodingKey {
let intValue: Int?
let stringValue: String

init?(intValue: Int) {
self.intValue = intValue
self.stringValue = intValue.description
}

init?(stringValue: String) {
return nil
}
}

mutating func encodeNil() throws {
data.encode(key: codingPath + [nextIndexedKey()], value: "nil")
}

mutating func encode(_ value: Bool) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: String) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value)
}

mutating func encode(_ value: Double) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: Float) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: Int) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: Int8) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: Int16) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: Int32) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: Int64) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: UInt) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: UInt8) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: UInt16) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: UInt32) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode(_ value: UInt64) throws {
data.encode(key: codingPath + [nextIndexedKey()], value: value.description)
}

mutating func encode<T: Encodable>(_ value: T) throws {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath = codingPath + [nextIndexedKey()]
try value.encode(to: stringsEncoding)
}

mutating func nestedContainer<NestedKey: CodingKey>(
keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
var container = StringsKeyedEncoding<NestedKey>(to: data)
container.codingPath = codingPath + [nextIndexedKey()]
return KeyedEncodingContainer(container)
}

mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
var container = StringsUnkeyedEncoding(to: data)
container.codingPath = codingPath + [nextIndexedKey()]
return container
}

mutating func superEncoder() -> Encoder {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath.append(nextIndexedKey())
return stringsEncoding
}
}
fileprivate struct StringsSingleValueEncoding: SingleValueEncodingContainer {

private let data: StringsEncoding.Data

init(to data: StringsEncoding.Data) {
self.data = data
}

var codingPath: [CodingKey] = []

mutating func encodeNil() throws {
data.encode(key: codingPath, value: "nil")
}

mutating func encode(_ value: Bool) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: String) throws {
data.encode(key: codingPath, value: value)
}

mutating func encode(_ value: Double) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: Float) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: Int) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: Int8) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: Int16) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: Int32) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: Int64) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: UInt) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: UInt8) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: UInt16) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: UInt32) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode(_ value: UInt64) throws {
data.encode(key: codingPath, value: value.description)
}

mutating func encode<T: Encodable>(_ value: T) throws {
var stringsEncoding = StringsEncoding(to: data)
stringsEncoding.codingPath = codingPath
try value.encode(to: stringsEncoding)
}
}

Obviously, I made some design decisions regarding how to encode nested types using the (very!) simple strings file format. Hopefully, my code is clear enough that it should be easy to tweak the encoding details if so desired.

Tests

A simple test for a trivial Codable type:

struct Product: Codable {
var name: String
var price: Float
var info: String
}

let iPhone = Product(name: "iPhone X", price: 1_000, info: "Our best iPhone yet!")

let stringsEncoder = StringsEncoder()
do {
let stringsFile = try stringsEncoder.encode(iPhone)
print(stringsFile)
} catch {
print("Encoding failed: \(error)")
}

Output:

/* Generated by StringsEncoder */
"info" = "Our best iPhone yet!";
"name" = "iPhone X";
"price" = "1000.0";

A more complex test with nested structs and arrays:

struct Product: Codable {
var name: String
var price: Float
var info: String
}

struct Address: Codable {
var street: String
var city: String
var state: String
}

struct Store: Codable {
var name: String
var address: Address // nested struct
var products: [Product] // array
}

let iPhone = Product(name: "iPhone X", price: 1_000, info: "Our best iPhone yet!")
let macBook = Product(name: "Mac Book Pro", price: 2_000, info: "Early 2019")
let watch = Product(name: "Apple Watch", price: 500, info: "Series 4")

let appleStore = Store(
name: "Apple Store",
address: Address(street: "300 Post Street", city: "San Francisco", state: "CA"),
products: [iPhone, macBook, watch]
)

let stringsEncoder = StringsEncoder()
do {
let stringsFile = try stringsEncoder.encode(appleStore)
print(stringsFile)
} catch {
print("Encoding failed: \(error)")
}

Output:

/* Generated by StringsEncoder */
"address.city" = "San Francisco";
"address.state" = "CA";
"address.street" = "300 Post Street";
"name" = "Apple Store";
"products.0.info" = "Our best iPhone yet!";
"products.0.name" = "iPhone X";
"products.0.price" = "1000.0";
"products.1.info" = "Early 2019";
"products.1.name" = "Mac Book Pro";
"products.1.price" = "2000.0";
"products.2.info" = "Series 4";
"products.2.name" = "Apple Watch";
"products.2.price" = "500.0";

Decoding

Given how big this answer already is, I'm gonna to leave the decoding part (i.e., creating the StringsDecoder class, conforming to the Decoder protocol, etc) as an exercise to the reader... please let me know if you guys need any help with that and I will post a complete solution later ;)

Swift struct with custom encoder and decoder cannot conform to 'Encodable'

Your encode function is incorrectly trying to encode a type, ValueAndRange.self, rather than a value.

Looking at the init(from:) method I think your encode function should look something like this

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: EffectKeys.self)

switch self {
case .lowPassFilter(let effect):
try container.encode(effect.cutOffFrequency, forKey: .cutoffFrequency)
try container.encode(effect.resonance, forKey: .resonance)
}
}

I didn't include .effectType in this code since I am uncertain of its usage (isn't it always the same hard coded string?).

Field level custom decoder

For example we have an example json:

let json = """
{
"id": 1,
"title": "Title",
"thumbnail": "https://www.sample-videos.com/img/Sample-jpg-image-500kb.jpg",
"date": "2014-07-15"
}
""".data(using: .utf8)!

If we want parse that, we can use Codable protocol and simple NewsCodable struct:

public struct NewsCodable: Codable {
public let id: Int
public let title: String
public let thumbnail: PercentEncodedUrl
public let date: MyDate
}

PercentEncodedUrl is our custom Codable wrapper for URL, which adding percent encoding to url string. Standard URL does not support that out of the box.

public struct PercentEncodedUrl: Codable {
public let url: URL

public init(from decoder: Decoder) throws {
let urlString = try decoder.singleValueContainer().decode(String.self)

guard
let encodedUrlString = urlString.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
let url = URL.init(string: encodedUrlString) else {
throw PercentEncodedUrlError.url(urlString)
}

self.url = url
}

public enum PercentEncodedUrlError: Error {
case url(String)
}
}

If some strange reasons we need custom decoder for date string(Date decoding has plenty of support in JSONDecoder), we can provide wrapper like PercentEncodedUrl.

public struct MyDate: Codable {
public let date: Date

public init(from decoder: Decoder) throws {
let dateString = try decoder.singleValueContainer().decode(String.self)

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
guard let date = dateFormatter.date(from: dateString) else {
throw MyDateError.date(dateString)
}

self.date = date
}

public enum MyDateError: Error {
case date(String)
}
}


let decoder = JSONDecoder()
let news = try! decoder.decode(NewsCodable.self, from: json)

So we provided field level custom decoder.

Implementing a custom Decoder in Swift 4

I haven't had a chance to turn my code into a framework yet, but you can take a look at my Github Repository that implements both a custom decoder and encoder for XML.

Link: https://github.com/ShawnMoore/XMLParsing

The encoder and decoder resides in the XML folder of the repo. It is based on Apple's JSONEncoder and JSONDecoder with changes to fit the XML standard.


Differences between XMLDecoder and JSONDecoder

  1. XMLDecoder.DateDecodingStrategy has an extra case titled keyFormatted. This case takes a closure that gives you a CodingKey, and it is up to you to provide the correct DateFormatter for the provided key. This is simply a convenience case on the DateDecodingStrategy of JSONDecoder.
  2. XMLDecoder.DataDecodingStrategy has an extra case titled keyFormatted. This case takes a closure that gives you a CodingKey, and it is up to you to provide the correct data or nil for the provided key. This is simply a convenience case on the DataDecodingStrategy of JSONDecoder.
  3. If the object conforming to the Codable protocol has an array, and the XML being parsed does not contain the array element, XMLDecoder will assign an empty array to the attribute. This is because the XML standard says if the XML does not contain the attribute, that could mean that there are zero of those elements.

Differences between XMLEncoder and JSONEncoder

  1. Contains an option called StringEncodingStrategy, this enum has two options, deferredToString and cdata. The deferredToString option is default and will encode strings as simple strings. If cdata is selected, all strings will be encoded as CData.

  2. The encode function takes in two additional parameters than JSONEncoder does. The first additional parameter in the function is a RootKey string that will have the entire XML wrapped in an element named that key. This parameter is required. The second parameter is an XMLHeader, which is an optional parameter that can take the version, encoding strategy and standalone status, if you want to include this information in the encoded xml.



Examples

For a full list of examples, see the Sample XML folder in the repository.

XML To Parse:

<?xml version="1.0"?>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>

Swift Structs:

struct Book: Codable {
var id: String
var author: String
var title: String
var genre: Genre
var price: Double
var publishDate: Date
var description: String

enum CodingKeys: String, CodingKey {
case id, author, title, genre, price, description

case publishDate = "publish_date"
}
}

enum Genre: String, Codable {
case computer = "Computer"
case fantasy = "Fantasy"
case romance = "Romance"
case horror = "Horror"
case sciFi = "Science Fiction"
}

XMLDecoder:

let data = Data(forResource: "book", withExtension: "xml") else { return nil }

let decoder = XMLDecoder()

let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()

decoder.dateDecodingStrategy = .formatted(formatter)

do {
let book = try decoder.decode(Book.self, from: data)
} catch {
print(error)
}

XMLEncoder:

let encoder = XMLEncoder()

let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
return formatter
}()

encoder.dateEncodingStrategy = .formatted(formatter)

do {
let data = try encoder.encode(self, withRootKey: "book", header: XMLHeader(version: 1.0))

print(String(data: data, encoding: .utf8))
} catch {
print(error)
}

What is superEncoder for in UnkeyedEncodingContainer and KeyedEncodingContainerProtocol?

superEncoder in encoders and superDecoder in decoders is a way to be able to "reserve" a nested container inside of a container, without knowing what type it will be ahead of time.

One of the main purposes for this is to support inheritance in Encodable/Decodable classes: a class T: Encodable may choose to encode its contents into an UnkeyedContainer, but its subclass U: T may choose to encode its contents into a KeyedContainer.

In U.encode(to:), U will need to call super.encode(to:), and pass in an Encoder — but it cannot pass in the Encoder that it has received, because it has already encoded its contents in a keyed way, and it is invalid for T to request an unkeyed container from that Encoder. (And in general, U won't even know what kind of container T might want.)

The escape hatch, then, is for U to ask its container for a nested Encoder to be able to pass that along to its superclass. The container will make space for a nested value and create a new Encoder which allows for writing into that reserved space. T can then use that nested Encoder to encode however it would like.

The result ends up looking as if U requested a nested container and encoded the values of T into it.


To make this a bit more concrete, consider the following:

import Foundation

class T: Encodable {
let x, y: Int
init(x: Int, y: Int) { self.x = x; self.y = y }

func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(x)
try container.encode(y)
}
}

class U: T {
let z: Int
init(x: Int, y: Int, z: Int) { self.z = z; super.init(x: x, y: y) }

enum CodingKeys: CodingKey { case z }

override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(z, forKey: .z)

/* How to encode T.x and T.y? */
}
}

let u = U(x: 1, y: 2, z: 3)
let data = try JSONEncoder().encode(u)
print(String(data: data, encoding: .utf8))

U has a few options for how to encode x and y:

  1. It can truly override the encoding policy of T by including x and y in its CodingKeys enum and encode them directly. This ignores how T would prefer to encode, and if decoding is required, means that you'll have to be able to create a new T without calling its init(from:)

  2. It can call super.encode(to: encoder) to have the superclass encode into the same encoder that it does. In this case, this will crash, since U has already requested a keyed container from encoder, and calling T.encode(to:) will immediately request an unkeyed container from the same encoder

    • In general, this may work if T and U both request the same container type, but it's really not recommended to rely on. Depending on how T encodes, it may override values that U has already encoded
  3. Nest T inside of the keyed container with super.encode(to: container.superEncoder()); this will reserve a spot in the container dictionary, create a new Encoder, and have T write to that encoder. The result of this, in JSON, will be:

    { "z": 3,
    "super": [1, 2] }

Trying to make a class codable in Swift but it is not the correct format?

You need to pass in the array to decode, don't pass in the array element type, then try to force-cast that to an array, that doesn't make any sense. YourType and Array<YourType> are two different and completely unrelated types, so you cannot cast one to the other and you need to use the specific type when calling JSONDecoder.decode(_:from:).

let array = try JSONDecoder().decode([Attribute].self, from: data)

Btw as already pointed out in your previous question, there is no need to manually write the init(from:) and encode(to:) methods or the CodingKeys enum since for your simple type, the compiler can auto-synthesise all of those for you. Also, if you used a struct instead of class, you'd also get the member wise initialiser for free.

How does the encoded data end up in the Encoder, when it has no mutating functions?

Is the Encoder somehow secretly passed as a reference instead of as a value?

Close, but not quite. JSONEncoder and its underlying containers are relatively thin veneer interfaces for reference-based storage. In the swift-corelibs-foundation implementation used on non-Darwin platforms, this is done using RefArray and RefObject to store keyed objects; on Darwin platforms, the same is done using NSMutableArray and NSMutableDictionary. It's these reference objects that are passed around and shared, so while the individual Encoders and containers can be passed around, they're writing to shared storage.

But it should be copy-on-write as everything else in Swift, which means the data shouldn't be able to make its way back up the chain!

One more note on this: all of the relevant types internal to this implementation are all classes (e.g. JSONEncoderImpl in swift-corelibs-foundation, _JSONEncoder on Darwin), which means that they wouldn't be copy-on-write, but rather, passed by reference anyway.



Related Topics



Leave a reply



Submit