Encode/Decode Array of Types conforming to protocol with JSONEncoder
The reason why your first example doesn't compile (and your second crashes) is because protocols don't conform to themselves – Tag
is not a type that conforms to Codable
, therefore neither is [Tag]
. Therefore Article
doesn't get an auto-generated Codable
conformance, as not all of its properties conform to Codable
.
Encoding and decoding only the properties listed in the protocol
If you just want to encode and decode the properties listed in the protocol, one solution would be to simply use an AnyTag
type-eraser that just holds those properties, and can then provide the Codable
conformance.
You can then have Article
hold an array of this type-erased wrapper, rather than of Tag
:
struct AnyTag : Tag, Codable {
let type: String
let value: String
init(_ base: Tag) {
self.type = base.type
self.value = base.value
}
}
struct Article: Codable {
let tags: [AnyTag]
let title: String
}
let tags: [Tag] = [
AuthorTag(value: "Author Tag Value"),
GenreTag(value:"Genre Tag Value")
]
let article = Article(tags: tags.map(AnyTag.init), title: "Article Title")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try jsonEncoder.encode(article)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
Which outputs the following JSON string:
{
"title" : "Article Title",
"tags" : [
{
"type" : "author",
"value" : "Author Tag Value"
},
{
"type" : "genre",
"value" : "Genre Tag Value"
}
]
}
and can be decoded like so:
let decoded = try JSONDecoder().decode(Article.self, from: jsonData)
print(decoded)
// Article(tags: [
// AnyTag(type: "author", value: "Author Tag Value"),
// AnyTag(type: "genre", value: "Genre Tag Value")
// ], title: "Article Title")
Encoding and decoding all properties of the conforming type
If however you need to encode and decoded every property of the given Tag
conforming type, you'll likely want to store the type information in the JSON somehow.
I would use an enum
in order to do this:
enum TagType : String, Codable {
// be careful not to rename these – the encoding/decoding relies on the string
// values of the cases. If you want the decoding to be reliant on case
// position rather than name, then you can change to enum TagType : Int.
// (the advantage of the String rawValue is that the JSON is more readable)
case author, genre
var metatype: Tag.Type {
switch self {
case .author:
return AuthorTag.self
case .genre:
return GenreTag.self
}
}
}
Which is better than just using plain strings to represent the types, as the compiler can check that we've provided a metatype for each case.
Then you just have to change the Tag
protocol such that it requires conforming types to implement a static
property that describes their type:
protocol Tag : Codable {
static var type: TagType { get }
var value: String { get }
}
struct AuthorTag : Tag {
static var type = TagType.author
let value: String
var foo: Float
}
struct GenreTag : Tag {
static var type = TagType.genre
let value: String
var baz: String
}
Then we need to adapt the implementation of the type-erased wrapper in order to encode and decode the TagType
along with the base Tag
:
struct AnyTag : Codable {
var base: Tag
init(_ base: Tag) {
self.base = base
}
private enum CodingKeys : CodingKey {
case type, base
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(TagType.self, forKey: .type)
self.base = try type.metatype.init(from: container.superDecoder(forKey: .base))
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type(of: base).type, forKey: .type)
try base.encode(to: container.superEncoder(forKey: .base))
}
}
We're using a super encoder/decoder in order to ensure that the property keys for the given conforming type don't conflict with the key used to encode the type. For example, the encoded JSON will look like this:
{
"type" : "author",
"base" : {
"value" : "Author Tag Value",
"foo" : 56.7
}
}
If however you know there won't be a conflict, and want the properties to be encoded/decoded at the same level as the "type" key, such that the JSON looks like this:
{
"type" : "author",
"value" : "Author Tag Value",
"foo" : 56.7
}
You can pass decoder
instead of container.superDecoder(forKey: .base)
& encoder
instead of container.superEncoder(forKey: .base)
in the above code.
As an optional step, we could then customise the Codable
implementation of Article
such that rather than relying on an auto-generated conformance with the tags
property being of type [AnyTag]
, we can provide our own implementation that boxes up a [Tag]
into an [AnyTag]
before encoding, and then unbox for decoding:
struct Article {
let tags: [Tag]
let title: String
init(tags: [Tag], title: String) {
self.tags = tags
self.title = title
}
}
extension Article : Codable {
private enum CodingKeys : CodingKey {
case tags, title
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.tags = try container.decode([AnyTag].self, forKey: .tags).map { $0.base }
self.title = try container.decode(String.self, forKey: .title)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(tags.map(AnyTag.init), forKey: .tags)
try container.encode(title, forKey: .title)
}
}
This then allows us to have the tags
property be of type [Tag]
, rather than [AnyTag]
.
Now we can encode and decode any Tag
conforming type that's listed in our TagType
enum:
let tags: [Tag] = [
AuthorTag(value: "Author Tag Value", foo: 56.7),
GenreTag(value:"Genre Tag Value", baz: "hello world")
]
let article = Article(tags: tags, title: "Article Title")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try jsonEncoder.encode(article)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
Which outputs the JSON string:
{
"title" : "Article Title",
"tags" : [
{
"type" : "author",
"base" : {
"value" : "Author Tag Value",
"foo" : 56.7
}
},
{
"type" : "genre",
"base" : {
"value" : "Genre Tag Value",
"baz" : "hello world"
}
}
]
}
and can then be decoded like so:
let decoded = try JSONDecoder().decode(Article.self, from: jsonData)
print(decoded)
// Article(tags: [
// AuthorTag(value: "Author Tag Value", foo: 56.7000008),
// GenreTag(value: "Genre Tag Value", baz: "hello world")
// ],
// title: "Article Title")
How can I use Swift’s Codable to encode into a dictionary?
If you don't mind a bit of shifting of data around you could use something like this:
extension Encodable {
func asDictionary() throws -> [String: Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
throw NSError()
}
return dictionary
}
}
Or an optional variant
extension Encodable {
var dictionary: [String: Any]? {
guard let data = try? JSONEncoder().encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
Assuming Foo
conforms to Codable
or really Encodable
then you can do this.
let struct = Foo(a: 1, b: 2)
let dict = try struct.asDictionary()
let optionalDict = struct.dictionary
If you want to go the other way(init(any)
), take a look at this Init an object conforming to Codable with a dictionary/array
Type 'X' does not conform to protocol 'Encodable'
I google and found an article, which provide implementations for un-codable CLLocation
.
After reading that article, it's hard to implement Decodable
for CLLocation. But the author use another struct Location
for decoding CLLocation
object. It's funny and tricky.
For Encodable
extension CLLocation: Encodable {
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case altitude
case horizontalAccuracy
case verticalAccuracy
case speed
case course
case timestamp
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(coordinate.latitude, forKey: .latitude)
try container.encode(coordinate.longitude, forKey: .longitude)
try container.encode(altitude, forKey: .altitude)
try container.encode(horizontalAccuracy, forKey: .horizontalAccuracy)
try container.encode(verticalAccuracy, forKey: .verticalAccuracy)
try container.encode(speed, forKey: .speed)
try container.encode(course, forKey: .course)
try container.encode(timestamp, forKey: .timestamp)
}
}
For Decodable
struct Location: Codable {
let latitude: CLLocationDegrees
let longitude: CLLocationDegrees
let altitude: CLLocationDistance
let horizontalAccuracy: CLLocationAccuracy
let verticalAccuracy: CLLocationAccuracy
let speed: CLLocationSpeed
let course: CLLocationDirection
let timestamp: Date
}
extension CLLocation {
convenience init(model: Location) {
self.init(coordinate: CLLocationCoordinate2DMake(model.latitude, model.longitude), altitude: model.altitude, horizontalAccuracy: model.horizontalAccuracy, verticalAccuracy: model.verticalAccuracy, course: model.course, speed: model.speed, timestamp: model.timestamp)
}
}
///
struct Person {
let name: String
let location: CLLocation
enum CodingKeys: String, CodingKey {
case name
case location
}
}
extension Person: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let name = try values.decode(String.self, forKey: .name)
// Decode to `Location` struct, and then convert back to `CLLocation`.
// It's very tricky
let locationModel = try values.decode(Location.self, forKey: .location)
location = CLLocation(model: locationModel)
}
}
class method requires that 'AFDataResponse X ' conform to Encodable & Encodable
In the API you've described, the onSuccess
closure would take a Feature
value, not an AFDataResponse<Feature>
value.
With JSONDecoder in Swift 4, can missing keys use a default value instead of having to be optional properties?
Approach that I prefer is using so called DTOs - data transfer object.
It is a struct, that conforms to Codable and represents the desired object.
struct MyClassDTO: Codable {
let items: [String]?
let otherVar: Int?
}
Then you simply init the object that you want to use in the app with that DTO.
class MyClass {
let items: [String]
var otherVar = 3
init(_ dto: MyClassDTO) {
items = dto.items ?? [String]()
otherVar = dto.otherVar ?? 3
}
var dto: MyClassDTO {
return MyClassDTO(items: items, otherVar: otherVar)
}
}
This approach is also good since you can rename and change final object however you wish to.
It is clear and requires less code than manual decoding.
Moreover, with this approach you can separate networking layer from other app.
Related Topics
How to Open to a Specific View Using Home Quick Actions
How to Await X Seconds with Async Await Swift 5.5
Nscollectionviewitem Never Instantiate
Can You Enforce a Typealias in Swift
Error with Parse Query Findobjectsinbackgroundwithblock
Why Can't I Use Subscripting on a Ckrecord Object in Swift
Swift Error Using Initialized Properties in Expressions Before Super.Init
Passing a Variable Through a Segue? Xcode 8 Swift 3
How to Save a Custom Class as an Attribute of a Coredata Entity in Swift 3
Images Inaccessible from Asset Catalog in a Swiftui Framework
Cannot Invoke Initializer for Type 'Sqlite3_Destructor_Type'
Swiftui - Make Toolbar's Navigationlink Use Detail View
Does the Initializer of an 'Open' Class Need to Be Open as Well
Swiftui View Property Willset & Didset Property Observers Not Working