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
XMLDecoder.DateDecodingStrategy
has an extra case titledkeyFormatted
. 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.XMLDecoder.DataDecodingStrategy
has an extra case titledkeyFormatted
. 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.- 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
Contains an option called
StringEncodingStrategy
, this enum has two options,deferredToString
andcdata
. The deferredToString option is default and will encode strings as simple strings. If cdata is selected, all strings will be encoded as CData.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
:
It can truly override the encoding policy of
T
by includingx
andy
in itsCodingKeys
enum and encode them directly. This ignores howT
would prefer to encode, and if decoding is required, means that you'll have to be able to create a newT
without calling itsinit(from:)
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, sinceU
has already requested a keyed container fromencoder
, and callingT.encode(to:)
will immediately request an unkeyed container from the same encoder- In general, this may work if
T
andU
both request the same container type, but it's really not recommended to rely on. Depending on howT
encodes, it may override values thatU
has already encoded
- In general, this may work if
Nest
T
inside of the keyed container withsuper.encode(to: container.superEncoder())
; this will reserve a spot in the container dictionary, create a newEncoder
, and haveT
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 Encoder
s 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
Checking If a Value Is Changed Using Kvo in Swift 3
Macos Menubar Application: Main Menu Not Being Displayed
How to Open a Nspopover at a Distance from the System Bar
Getting Dyld_Fatal_Error After Updating to Xcode 6 Beta 4 Using Swift
How to Run Xctest for a Swift Application from the Command Line
Differencebetween Http Parameters and Http Headers
Storing Different Types of Value in Array in Swift
Differencebetween Type Safety and Type Inference
How to Use Combine to Track Uitextfield Changes in a Uiviewrepresentable Class
Write and Read a Plist in Swift with Simple Data
How to Compare "Any" Value Types
Prevent Nsurlsession from Caching Responses
Swift 2 Migration Savecontext() in Appdelegate
Can't Create a Range in Swift 3
Swift: How to Create External Interface for Static Library (Public Headers Analog in Objective-C .H)
Initialize Lazy Instance Variable with Value That Depends on Other Instance Variables