Swift & Codable: how to "bypass" some JSON levels? - json

I would like to use this Pokémon API to fetch some data and convert it into a Swift Pokemon struct.
Here is an extract of the response I get when fetching Pokemon #142:
{
"id": 142,
"name": "aerodactyl",
"types": [{
"type": {
"name": "rock",
"url": "https://pokeapi.co/api/v2/type/6/"
},
"slot": 1
},
{
"type": {
"name": "flying",
"url": "https://pokeapi.co/api/v2/type/3/"
},
"slot": 2
}
]
}
Here is the struct I wrote to convert this JSON into a Swift type:
struct Pokemon: Codable {
var id: Int
let name: String
var types: [PokemonType]?
}
struct PokemonType: Codable {
var type: PokemonTypeContent
}
struct PokemonTypeContent: Codable {
var name: PokemonTypeNameContent
}
enum PokemonTypeNameContent: String, Codable {
case flying = "flying"
case rock = "rock"
// ...
}
Now here is my problem: when I want to get the Pokemon types, I need to dig into this:
pokemon.types.first?.type.name
I would like to know if I have instead a way of getting the PokemonTypeNameContent array in the Pokemon struct, to do something like this:
struct Pokemon {
var types: [PokemonTypeNameContent]?
}
(I am not interested in getting the slot values).
Thank you for your help!

You can do custom encoding for PokemonTypeNameContent, and traverse through the levels of JSON using nestedContainer
enum PokemonTypeNameContent: String, Decodable {
case flying = "flying"
case rock = "rock"
// ...
enum OuterCodingKeys: CodingKey { case type }
enum InnerCodingKeys: CodingKey { case name }
init(from decoder: Decoder) throws {
// this is the container for each JSON object in the "types" array
let container = try decoder.container(keyedBy: OuterCodingKeys.self)
// this finds the nested container (i.e. JSON object) associated with the key "type"
let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
// now we can decode "name" as a string
let name = try innerContainer.decode(String.self, forKey: .name)
if let pokemonType = Self.init(rawValue: name) {
self = pokemonType
} else {
throw DecodingError.typeMismatch(
PokemonTypeNameContent.self,
.init(codingPath: innerContainer.codingPath + [InnerCodingKeys.name],
debugDescription: "Unknown pokemon type '\(name)'",
underlyingError: nil
)
)
}
}
}
// Pokemon can then be declared like this:
struct Pokemon: Decodable {
let id: Int
let name: String
let types: [PokemonTypeNameContent]
}
Do note that this means that you lose the option of decoding PokemonTypeNameContent as a regular enum. If you do want to do that, put the custom decoding code into a property wrapper. Note that we would be decoding the entire JSON array, instead of each JSON object.
#propertyWrapper
struct DecodePokemonTypes: Decodable {
var wrappedValue: [PokemonTypeNameContent]
init(wrappedValue: [PokemonTypeNameContent]) {
self.wrappedValue = wrappedValue
}
enum OuterCodingKeys: CodingKey { case type }
enum InnerCodingKeys: CodingKey { case name }
init(from decoder: Decoder) throws {
// container for the "types" JSON array
var unkeyedContainer = try decoder.unkeyedContainer()
wrappedValue = []
// while we are not at the end of the JSON array
while !unkeyedContainer.isAtEnd {
// similar to the first code snippet
let container = try unkeyedContainer.nestedContainer(keyedBy: OuterCodingKeys.self)
let innerContainer = try container.nestedContainer(keyedBy: InnerCodingKeys.self, forKey: .type)
let name = try innerContainer.decode(String.self, forKey: .name)
if let pokemonType = PokemonTypeNameContent(rawValue: name) {
wrappedValue.append(pokemonType)
} else {
throw DecodingError.typeMismatch(
PokemonTypeNameContent.self,
.init(codingPath: innerContainer.codingPath + [InnerCodingKeys.name],
debugDescription: "Unknown pokemon type '\(name)'",
underlyingError: nil
)
)
}
}
}
}
// You would write this in Pokemon
#DecodePokemonTypes
var types: [PokemonTypeNameContent]

Related

Decoding JSON structures with minor differences into just one format

I have three API endpoints that result in the same final structure, however the full JSON structure received from the API is a bit different in each of them.
First JSON result
{
"tracks": [{
"name": "Never Gonna Give You Up"
}]
}
Second JSON result
{
"items": [{
"name": "Never Gonna Give You Up"
}]
}
Third JSON result
{
"items": [{
"track": {
"name": "Never Gonna Give You Up"
}
}]
}
I want all of them to look like this
{
"tracks": [{
"name": "Never Gonna Give You Up"
}]
}
For that I'm using three different structures:
First:
struct TopHitsTrackResponse: Decodable {
var tracks: [Track]
}
Second:
struct FavoritesTrackResponse: Decodable {
var tracks: [Track]
enum CodingKeys: String, CodingKey {
case tracks = "items"
}
}
And the third one is the code below.
What I've tried
I have successfully made the first and second JSON results look exactly equal to the wanted result. However, the third one is a bit more complicated for me. Here's what I've tried without success.
struct NestedTrackResponse: Decodable {
let tracks: [Track]
enum CodingKeys: String, CodingKey {
case tracks = "items"
}
enum TrackKeys: String, CodingKey {
case track
}
init(from decoder: Decoder) throws {
let outerContainer = try decoder.container(keyedBy: CodingKeys.self)
let trackContainer = try outerContainer.nestedContainer(keyedBy: TrackKeys.self,
forKey: .tracks)
self.tracks = try trackContainer.decode([Track].self, forKey: .track)
}
struct Track: Decodable {
var name: String
}
}
I'm calling the API with this function
AF.request(urlRequest)
.validate()
.responseDecodable(of: NestedTrackResponse.self) { response in
// It's always resulting in fatal error
guard let data = response.value else {
fatalError("Error receiving tracks from API.")
}
}
// - `AF` is Alamofire, but I've already tried not using it,
// and the error persists
// - `urlRequest` is just the URL of the API and the API key,
// doesn't really matter for this problem
And getting this error
Expected to decode Dictionary<String, Any> but found an array instead.
You have to write a custom initializer with if let expressions to distinguish the cases.
struct Response : Decodable {
let tracks : [Track]
private enum CodingKeys : String, CodingKey { case items, tracks }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let tracks = try? container.decode([Track].self, forKey: .items) {
self.tracks = tracks
} else if let tracks = try? container.decode([Track].self, forKey: .tracks) {
self.tracks = tracks
} else if let items = try? container.decode([Item].self, forKey: .items) {
self.tracks = items.map(\.track)
} else {
throw DecodingError.dataCorruptedError(forKey: .items, in: container, debugDescription: "Unsupported JSON structure")
}
}
}
struct Item : Decodable {
let track : Track
}
struct Track : Decodable {
let name : String
}
As I said in the other answer, recommending to stick to the JSON structure that you receive from the API, this will prevent many headaches in future, as well as having a well-defined networking layer.
struct Track: Decodable {
var name: String
}
struct TopHitsTrackResponse: Decodable {
var tracks: [Track]
}
struct FavoritesTrackResponse: Decodable {
var items: [Track]
}
// though you should name this by the API name,
// like with the other two
struct NestedTrackResponse: Decodable {
var items: [Item]
struct Item: Decodable {
var track: Track
}
}
Basically the above is your networking layer. Now, comes the business layer, which will try to extract the tracks from each kind of response.
You could implement it with protocols, but functions work as well (if not even better):
func tracks(from response: TopHitsTrackResponse) -> [Track] {
response.tracks
}
func tracks(from response: FavoritesTrackResponse) -> [Track] {
response.items
}
func tracks(from response: NestedTrackResponse) -> [Track] {
response.items.map(\.track)
}
Then, in your API callbacks, you simply call tracks(from:), in order to extract the most wanted results.

Parsing complex JSON where data and "column headers" are in separate arrays

I have the following JSON data I get from an API:
{"datatable":
{"data" : [
["John", "Doe", "1990-01-01", "Chicago"],
["Jane", "Doe", "2000-01-01", "San Diego"]
],
"columns": [
{ "name": "First", "type": "String" },
{ "name": "Last", "type": "String" },
{ "name": "Birthday", "type": "Date" },
{ "name": "City", "type": "String" }
]}
}
A later query could result the following:
{"datatable":
{"data" : [
["Chicago", "Doe", "John", "1990-01-01"],
["San Diego", "Doe", "Jane", "2000-01-01"]
],
"columns": [
{ "name": "City", "type": "String" },
{ "name": "Last", "type": "String" },
{ "name": "First", "type": "String" },
{ "name": "Birthday", "type": "Date" }
]
}
}
The order of the colums seems to be fluid.
I initially wanted to decode the JSON with JSONDecoder, but for that I need the data array to be a dictionary and not an array.
The only other method I could think of was to convert the result to a dictionary with something like:
extension String {
func convertToDictionary() -> [String: Any]? {
if let data = data(using: .utf8) {
return try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
}
return nil
}
}
This will cause me however to have a lot of nested if let statements like if let x = dictOfStr["datatable"] as? [String: Any] { ... }.
Not to mention the subsequent looping through the columns array to organize the data.
Is there a better solution?
Thanks
You could still use JSONDecoder, but you'd need to manually decode the data array.
To do that, you'd need to read the columns array, and then decode the data array using the ordering that you got from the columns array.
This is actually a nice use case for KeyPaths. You can create a mapping of columns to object properties, and this helps avoid a large switch statement.
So here's the setup:
struct DataRow {
var first, last, city: String?
var birthday: Date?
}
struct DataTable: Decodable {
var data: [DataRow] = []
// coding key for root level
private enum RootKeys: CodingKey { case datatable }
// coding key for columns and data
private enum CodingKeys: CodingKey { case data, columns }
// mapping of json fields to properties
private let fields: [String: PartialKeyPath<DataRow>] = [
"First": \DataRow.first,
"Last": \DataRow.last,
"City": \DataRow.city,
"Birthday": \DataRow.birthday ]
// I'm actually ignoring here the type property in JSON
private struct Column: Decodable { let name: String }
// init ...
}
Now the init function:
init(from decoder: Decoder) throws {
let root = try decoder.container(keyedBy: RootKeys.self)
let inner = try root.nestedContainer(keyedBy: CodingKeys.self, forKey: .datatable)
let columns = try inner.decode([Column].self, forKey: .columns)
// for data, there's more work to do
var data = try inner.nestedUnkeyedContainer(forKey: .data)
// for each data row
while !data.isAtEnd {
let values = try data.decode([String].self)
var dataRow = DataRow()
// decode each property
for idx in 0..<values.count {
let keyPath = fields[columns[idx].name]
let value = values[idx]
// now need to decode a string value into the correct type
switch keyPath {
case let kp as WritableKeyPath<DataRow, String?>:
dataRow[keyPath: kp] = value
case let kp as WritableKeyPath<DataRow, Date?>:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "YYYY-MM-DD"
dataRow[keyPath: kp] = dateFormatter.date(from: value)
default: break
}
}
self.data.append(dataRow)
}
}
To use this, you'd use the normal JSONDecode way:
let jsonDecoder = JSONDecoder()
let dataTable = try jsonDecoder.decode(DataTable.self, from: jsonData)
print(dataTable.data[0].first) // prints John
print(dataTable.data[0].birthday) // prints 1990-01-01 05:00:00 +0000
EDIT
The code above assumes that all the values in a JSON array are strings and tries to do decode([String].self). If you can't make that assumption, you could decode the values to their underlying primitive types supported by JSON (number, string, bool, or null). It would look something like this:
enum JSONVal: Decodable {
case string(String), number(Double), bool(Bool), null, unknown
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let v = try? container.decode(String.self) {
self = .string(v)
} else if let v = try? container.decode(Double.self) {
self = .number(v)
} else if ...
// and so on, for null and bool
}
}
Then, in the code above, decode the array into these values:
let values = try data.decode([JSONValue].self)
Later when you need to use the value, you can examine the underlying value and decide what to do:
case let kp as WritableKeyPath<DataRow, Int?>:
switch value {
case number(let v):
// e.g. round the number and cast to Int
dataRow[keyPath: kp] = Int(v.rounded())
case string(let v):
// e.g. attempt to convert string to Int
dataRow[keyPath: kp] = Int((Double(str) ?? 0.0).rounded())
default: break
}
It appears that the data and columns values gets encoded in the same order so using that we can create a dictionary for column and array of values where each array is in the same order.
struct Root: Codable {
let datatable: Datatable
}
struct Datatable: Codable {
let data: [[String]]
let columns: [Column]
var columnValues: [Column: [String]]
enum CodingKeys: String, CodingKey {
case data, columns
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
data = try container.decode([[String]].self, forKey: .data)
columns = try container.decode([Column].self, forKey: .columns)
columnValues = [:]
data.forEach {
for i in 0..<$0.count {
columnValues[columns[i], default: []].append($0[i])
}
}
}
}
struct Column: Codable, Hashable {
let name: String
let type: String
}
Next step would be to introduce a struct for the data
The way I would do it is to create two model objects and have them both conform to the Codable protocol like so:
struct Datatable: Codable {
let data: [[String]]
let columns: [[String: String]]
}
struct JSONResponseType: Codable {
let datatable: Datatable
}
Then in your network call I'd decode the json response using JSONDecoder():
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let decodedData = try? decoder.decode(JSONResponseType.self, from: data) else {
// handle decoding failure
return
}
// do stuff with decodedData ex:
let datatable = decodedData.datatable
...
data in this case is the result from the URLSessionTask.
Let me know if this works.
Maybe try to save the given input inside a list of user objects? This way however the JSON is structured you can add them in the list and handle them after anyway you like. Maybe an initial alphabetical ordering after name would also help with the display order of users.
Here is a sample I wrote, instead of logging the info you can add a new UserObject to the list with the currently printed information.
let databaseData = table["datatable"]["data"];
let databaseColumns = table["datatable"]["columns"];
for (let key in databaseData) {
console.log(databaseColumns[0]["name"] + " = " + databaseData[key][0]);
console.log(databaseColumns[1]["name"] + " = " + databaseData[key][1]);
console.log(databaseColumns[2]["name"] + " = " + databaseData[key][2]);
console.log(databaseColumns[3]["name"] + " = " + databaseData[key][3]);
}
The only thing I could think of is:
struct ComplexValue {
var value:String
var columnName:String
var type:String
}
struct ComplexJSON: Decodable, Encodable {
enum CodingKeys: String, CodingKey {
case data, columns
}
var data:[[String]]
var columns:[ColumnSpec]
var processed:[[ComplexValue]]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
data = (try? container.decode([[String]].self, forKey: .data)) ?? []
columns = (try? container.decode([ColumnSpec].self, forKey: .columns)) ?? []
processed = []
for row in data {
var values = [ComplexValue]()
var i = 0
while i < columns.count {
var item = ComplexValue(value: row[i], columnName: columns[i].name, type: columns[i].type)
values.append(item)
i += 1
}
processed.append(values)
}
}
}
struct ColumnSpec: Decodable, Encodable {
enum CodingKeys: String, CodingKey {
case name, type
}
var name:String
var type:String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = (try? container.decode(String.self, forKey: .name)) ?? ""
type = (try? container.decode(String.self, forKey: .type)) ?? ""
}
}
Now you would have the processed variable which would contain formatted version of your data. Well, formatted might not be the best word, given that structure is completely dynamic, but at least whenever you extract some specific cell you would know its value, type and its column name.
I don't think you can do anything more specific than this without extra details about your APIs.
Also, please note that I did this in Playground, so some tweaks might be needed to make the code work in production. Although I think the idea is clearly visible.
P.S. My implementation does not deal with "datatable". Should be straightforward to add, but I thought it would only increase the length of my answer without providing any benefits. After all, the challenge is inside that field :)

Swift, How to Parse/Decode the JSON using Decodable and Codable, When key are unknow/dynamic

Below is my JSON, and I am not able to decode(using CodingKeys)
The data within the regions key is a Dictionary ("IN-WB", "IN-DL" & so on....), as the keys are dynamic, it can be changed more or less.
Please help me parsing the same using Decodable and Codable.
All the data should be within the single model.
{
"provider_code": "AIIN",
"name": "Jio India",
"regions": [
{
"IN-WB": "West Bengal"
},
{
"IN-DL": "Delhi NCR"
},
{
"IN-TN": "Tamil Nadu"
},
{
"IN": "India"
}
]
}
Just use a Dictionary for the regions.
struct Locations: Codable {
let providerCode: String
let name: String
let regions: [[String: String]]
enum CodingKeys: String, CodingKey {
case providerCode = "provider_code"
case name, regions
}
}
You cannot create a specific model for the regions as you wont know the property names
One of possible approach, without using dictionary. But still we have to found key at first )
I like this style as we can use Regions from beginning.
// example data.
let string = "{\"provider_code\":\"AIIN\",\"name\":\"Jio India\",\"regions\":[{\"IN-WB\":\"West Bengal\"},{\"IN-DL\":\"Delhi NCR\"},{\"IN-TN\":\"Tamil Nadu\"},{\"IN\":\"India\"}]}"
let data = string.data(using: .utf8)!
// little helper
struct DynamicGlobalKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
// model
struct Location: Decodable {
let providerCode: String
let name: String
let regions: [Region]
}
extension Location {
struct Region: Decodable {
let key: String
let name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicGlobalKey.self)
key = container.allKeys.first!.stringValue
name = try container.decode(String.self, forKey: container.allKeys.first!)
}
}
}
// example of decoding.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let location = try decoder.decode(Location.self, from: data)

How to decode a nested JSON struct with Swift Decodable protocol?

Here is my JSON
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
Here is the structure I want it saved to (incomplete)
struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int
enum CodingKeys: String, CodingKey {
case id,
// How do i get nested values?
}
}
I have looked at Apple's Documentation on decoding nested structs, but I still do not understand how to do the different levels of the JSON properly. Any help will be much appreciated.
Another approach is to create an intermediate model that closely matches the JSON (with the help of a tool like quicktype.io), let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:
// snake_case to match the JSON and hence no need to write CodingKey enums
fileprivate struct RawServerResponse: Decodable {
struct User: Decodable {
var user_name: String
var real_info: UserRealInfo
}
struct UserRealInfo: Decodable {
var full_name: String
}
struct Review: Decodable {
var count: Int
}
var id: Int
var user: User
var reviews_count: [Review]
}
struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int
init(from decoder: Decoder) throws {
let rawResponse = try RawServerResponse(from: decoder)
// Now you can pick items that are important to your data model,
// conveniently decoded into a Swift structure
id = String(rawResponse.id)
username = rawResponse.user.user_name
fullName = rawResponse.user.real_info.full_name
reviewCount = rawResponse.reviews_count.first!.count
}
}
This also allows you to easily iterate through reviews_count, should it contain more than 1 value in the future.
In order to solve your problem, you can split your RawServerResponse implementation into several logic parts (using Swift 5).
#1. Implement the properties and required coding keys
import Foundation
struct RawServerResponse {
enum RootKeys: String, CodingKey {
case id, user, reviewCount = "reviews_count"
}
enum UserKeys: String, CodingKey {
case userName = "user_name", realInfo = "real_info"
}
enum RealInfoKeys: String, CodingKey {
case fullName = "full_name"
}
enum ReviewCountKeys: String, CodingKey {
case count
}
let id: Int
let userName: String
let fullName: String
let reviewCount: Int
}
#2. Set the decoding strategy for id property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
// id
let container = try decoder.container(keyedBy: RootKeys.self)
id = try container.decode(Int.self, forKey: .id)
/* ... */
}
}
#3. Set the decoding strategy for userName property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
/* ... */
// userName
let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
userName = try userContainer.decode(String.self, forKey: .userName)
/* ... */
}
}
#4. Set the decoding strategy for fullName property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
/* ... */
// fullName
let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)
/* ... */
}
}
#5. Set the decoding strategy for reviewCount property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
/* ...*/
// reviewCount
var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
var reviewCountArray = [Int]()
while !reviewUnkeyedContainer.isAtEnd {
let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
}
guard let reviewCount = reviewCountArray.first else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
}
self.reviewCount = reviewCount
}
}
Complete implementation
import Foundation
struct RawServerResponse {
enum RootKeys: String, CodingKey {
case id, user, reviewCount = "reviews_count"
}
enum UserKeys: String, CodingKey {
case userName = "user_name", realInfo = "real_info"
}
enum RealInfoKeys: String, CodingKey {
case fullName = "full_name"
}
enum ReviewCountKeys: String, CodingKey {
case count
}
let id: Int
let userName: String
let fullName: String
let reviewCount: Int
}
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
// id
let container = try decoder.container(keyedBy: RootKeys.self)
id = try container.decode(Int.self, forKey: .id)
// userName
let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
userName = try userContainer.decode(String.self, forKey: .userName)
// fullName
let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)
// reviewCount
var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
var reviewCountArray = [Int]()
while !reviewUnkeyedContainer.isAtEnd {
let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
}
guard let reviewCount = reviewCountArray.first else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
}
self.reviewCount = reviewCount
}
}
Usage
let jsonString = """
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
"""
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let serverResponse = try! decoder.decode(RawServerResponse.self, from: jsonData)
dump(serverResponse)
/*
prints:
▿ RawServerResponse #1 in __lldb_expr_389
- id: 1
- user: "Tester"
- fullName: "Jon Doe"
- reviewCount: 4
*/
Rather than having one big CodingKeys enumeration with all the keys you'll need for decoding the JSON, I would advise splitting the keys up for each of your nested JSON objects, using nested enumerations to preserve the hierarchy:
// top-level JSON object keys
private enum CodingKeys : String, CodingKey {
// using camelCase case names, with snake_case raw values where necessary.
// the raw values are what's used as the actual keys for the JSON object,
// and default to the case name unless otherwise specified.
case id, user, reviewsCount = "reviews_count"
// "user" JSON object keys
enum User : String, CodingKey {
case username = "user_name", realInfo = "real_info"
// "real_info" JSON object keys
enum RealInfo : String, CodingKey {
case fullName = "full_name"
}
}
// nested JSON objects in "reviews" keys
enum ReviewsCount : String, CodingKey {
case count
}
}
This will make it easier to keep track of the keys at each level in your JSON.
Now, bearing in mind that:
A keyed container is used to decode a JSON object, and is decoded with a CodingKey conforming type (such as the ones we've defined above).
An unkeyed container is used to decode a JSON array, and is decoded sequentially (i.e each time you call a decode or nested container method on it, it advances to the next element in the array). See the second part of the answer for how you can iterate through one.
After getting your top-level keyed container from the decoder with container(keyedBy:) (as you have a JSON object at the top-level), you can repeatedly use the methods:
nestedContainer(keyedBy:forKey:) to get a nested object from an object for a given key
nestedUnkeyedContainer(forKey:) to get a nested array from an object for a given key
nestedContainer(keyedBy:) to get the next nested object from an array
nestedUnkeyedContainer() to get the next nested array from an array
For example:
struct ServerResponse : Decodable {
var id: Int, username: String, fullName: String, reviewCount: Int
private enum CodingKeys : String, CodingKey { /* see above definition in answer */ }
init(from decoder: Decoder) throws {
// top-level container
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
// container for { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
let userContainer =
try container.nestedContainer(keyedBy: CodingKeys.User.self, forKey: .user)
self.username = try userContainer.decode(String.self, forKey: .username)
// container for { "full_name": "Jon Doe" }
let realInfoContainer =
try userContainer.nestedContainer(keyedBy: CodingKeys.User.RealInfo.self,
forKey: .realInfo)
self.fullName = try realInfoContainer.decode(String.self, forKey: .fullName)
// container for [{ "count": 4 }] – must be a var, as calling a nested container
// method on it advances it to the next element.
var reviewCountContainer =
try container.nestedUnkeyedContainer(forKey: .reviewsCount)
// container for { "count" : 4 }
// (note that we're only considering the first element of the array)
let firstReviewCountContainer =
try reviewCountContainer.nestedContainer(keyedBy: CodingKeys.ReviewsCount.self)
self.reviewCount = try firstReviewCountContainer.decode(Int.self, forKey: .count)
}
}
Example decoding:
let jsonData = """
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
""".data(using: .utf8)!
do {
let response = try JSONDecoder().decode(ServerResponse.self, from: jsonData)
print(response)
} catch {
print(error)
}
// ServerResponse(id: 1, username: "Tester", fullName: "Jon Doe", reviewCount: 4)
Iterating through an unkeyed container
Considering the case where you want reviewCount to be an [Int], where each element represents the value for the "count" key in the nested JSON:
"reviews_count": [
{
"count": 4
},
{
"count": 5
}
]
You'll need to iterate through the nested unkeyed container, getting the nested keyed container at each iteration, and decoding the value for the "count" key. You can use the count property of the unkeyed container in order to pre-allocate the resultant array, and then the isAtEnd property to iterate through it.
For example:
struct ServerResponse : Decodable {
var id: Int
var username: String
var fullName: String
var reviewCounts = [Int]()
// ...
init(from decoder: Decoder) throws {
// ...
// container for [{ "count": 4 }, { "count": 5 }]
var reviewCountContainer =
try container.nestedUnkeyedContainer(forKey: .reviewsCount)
// pre-allocate the reviewCounts array if we can
if let count = reviewCountContainer.count {
self.reviewCounts.reserveCapacity(count)
}
// iterate through each of the nested keyed containers, getting the
// value for the "count" key, and appending to the array.
while !reviewCountContainer.isAtEnd {
// container for a single nested object in the array, e.g { "count": 4 }
let nestedReviewCountContainer = try reviewCountContainer.nestedContainer(
keyedBy: CodingKeys.ReviewsCount.self)
self.reviewCounts.append(
try nestedReviewCountContainer.decode(Int.self, forKey: .count)
)
}
}
}
Copy the json file to https://app.quicktype.io
Select Swift (if you use Swift 5, check the compatibility switch for Swift 5)
Use the following code to decode the file
Voila!
let file = "data.json"
guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else{
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else{
fatalError("Failed to locate \(file) in bundle.")
}
let yourObject = try? JSONDecoder().decode(YourModel.self, from: data)
Many good answers have already been posted, but there is a simpler method not described yet IMO.
When the JSON field names are written using snake_case_notation you can still use the camelCaseNotation in your Swift file.
You just need to set
decoder.keyDecodingStrategy = .convertFromSnakeCase
After this ☝️ line Swift will automatically match all the snake_case fields from the JSON to the camelCase fields in the Swift model.
E.g.
user_name` -> userName
reviews_count -> `reviewsCount
...
Here's the full code
1. Writing the Model
struct Response: Codable {
let id: Int
let user: User
let reviewsCount: [ReviewCount]
struct User: Codable {
let userName: String
struct RealInfo: Codable {
let fullName: String
}
}
struct ReviewCount: Codable {
let count: Int
}
}
2. Setting the Decoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
3. Decoding
do {
let response = try? decoder.decode(Response.self, from: data)
print(response)
} catch {
debugPrint(error)
}
Also you can use library KeyedCodable I prepared. It will require less code. Let me know what you think about it.
struct ServerResponse: Decodable, Keyedable {
var id: String!
var username: String!
var fullName: String!
var reviewCount: Int!
private struct ReviewsCount: Codable {
var count: Int
}
mutating func map(map: KeyMap) throws {
var id: Int!
try id <<- map["id"]
self.id = String(id)
try username <<- map["user.user_name"]
try fullName <<- map["user.real_info.full_name"]
var reviewCount: [ReviewsCount]!
try reviewCount <<- map["reviews_count"]
self.reviewCount = reviewCount[0].count
}
init(from decoder: Decoder) throws {
try KeyedDecoder(with: decoder).decode(to: &self)
}
}

Encode/Decode Array of Types conforming to protocol with JSONEncoder

I'm trying to find the best way to Encode/Decode an array of structs conforming to a swift protocol using the new JSONDecoder/Encoder in Swift 4.
I made up a little example to illustrate the problem:
First we have a protocol Tag and some Types that conform to this protocol.
protocol Tag: Codable {
var type: String { get }
var value: String { get }
}
struct AuthorTag: Tag {
let type = "author"
let value: String
}
struct GenreTag: Tag {
let type = "genre"
let value: String
}
Then we have a Type Article which has an Array of Tags.
struct Article: Codable {
let tags: [Tag]
let title: String
}
Finally we encode or decode the Article
let article = Article(tags: [AuthorTag(value: "Author Tag Value"), GenreTag(value:"Genre Tag Value")], title: "Article Title")
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(article)
let jsonString = String(data: jsonData, encoding: .utf8)
And this is the JSON structure that I like to have.
{
"title": "Article Title",
"tags": [
{
"type": "author",
"value": "Author Tag Value"
},
{
"type": "genre",
"value": "Genre Tag Value"
}
]
}
The problem is that at some point I have to switch on the type property to decode the Array but to Decode the Array I have to know its type.
EDIT:
It's clear to me why Decodable can not work out of the box but at least Encodable should work. The following modified Article struct compiles but crashes with the following error message.
fatal error: Array<Tag> does not conform to Encodable because Tag does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.43/src/swift/stdlib/public/core/Codable.swift, line 3280
struct Article: Encodable {
let tags: [Tag]
let title: String
enum CodingKeys: String, CodingKey {
case tags
case title
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(tags, forKey: .tags)
try container.encode(title, forKey: .title)
}
}
let article = Article(tags: [AuthorTag(value: "Author Tag"), GenreTag(value:"A Genre Tag")], title: "A Title")
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(article)
let jsonString = String(data: jsonData, encoding: .utf8)
And this is the relevant part from Codeable.swift
guard Element.self is Encodable.Type else {
preconditionFailure("\(type(of: self)) does not conform to Encodable because \(Element.self) does not conform to Encodable.")
}
Source: https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift
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")
Inspired by #Hamish answer. I found his approach reasonable, however few things might be improved:
Mapping array [Tag] to and from [AnyTag] in Article leave us without auto-generated Codable conformance
It's not possible to have same code for coding/encoding array of base class, since static var type can't be overridden in subclass. (for example if Tag would be super class of AuthorTag & GenreTag)
Most importantly this code can't be reused for another Type, you required to create new AnyAnotherType wrapper and it's internal coding/encoding.
I made slightly different solution, instead of wrapping each element of array, it's possible to make wrapper on entire array:
struct MetaArray<M: Meta>: Codable, ExpressibleByArrayLiteral {
let array: [M.Element]
init(_ array: [M.Element]) {
self.array = array
}
init(arrayLiteral elements: M.Element...) {
self.array = elements
}
enum CodingKeys: String, CodingKey {
case metatype
case object
}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [M.Element] = []
while !container.isAtEnd {
let nested = try container.nestedContainer(keyedBy: CodingKeys.self)
let metatype = try nested.decode(M.self, forKey: .metatype)
let superDecoder = try nested.superDecoder(forKey: .object)
let object = try metatype.type.init(from: superDecoder)
if let element = object as? M.Element {
elements.append(element)
}
}
array = elements
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try array.forEach { object in
let metatype = M.metatype(for: object)
var nested = container.nestedContainer(keyedBy: CodingKeys.self)
try nested.encode(metatype, forKey: .metatype)
let superEncoder = nested.superEncoder(forKey: .object)
let encodable = object as? Encodable
try encodable?.encode(to: superEncoder)
}
}
}
Where Meta is generic protocol:
protocol Meta: Codable {
associatedtype Element
static func metatype(for element: Element) -> Self
var type: Decodable.Type { get }
}
Now, storing tags will look like:
enum TagMetatype: String, Meta {
typealias Element = Tag
case author
case genre
static func metatype(for element: Tag) -> TagMetatype {
return element.metatype
}
var type: Decodable.Type {
switch self {
case .author: return AuthorTag.self
case .genre: return GenreTag.self
}
}
}
struct AuthorTag: Tag {
var metatype: TagMetatype { return .author } // keep computed to prevent auto-encoding
let value: String
}
struct GenreTag: Tag {
var metatype: TagMetatype { return .genre } // keep computed to prevent auto-encoding
let value: String
}
struct Article: Codable {
let title: String
let tags: MetaArray<TagMetatype>
}
Result JSON:
let article = Article(title: "Article Title",
tags: [AuthorTag(value: "Author Tag Value"),
GenreTag(value:"Genre Tag Value")])
{
"title" : "Article Title",
"tags" : [
{
"metatype" : "author",
"object" : {
"value" : "Author Tag Value"
}
},
{
"metatype" : "genre",
"object" : {
"value" : "Genre Tag Value"
}
}
]
}
And in case you want JSON to look even prettier:
{
"title" : "Article Title",
"tags" : [
{
"author" : {
"value" : "Author Tag Value"
}
},
{
"genre" : {
"value" : "Genre Tag Value"
}
}
]
}
Add to Meta protocol
protocol Meta: Codable {
associatedtype Element
static func metatype(for element: Element) -> Self
var type: Decodable.Type { get }
init?(rawValue: String)
var rawValue: String { get }
}
And replace CodingKeys with:
struct MetaArray<M: Meta>: Codable, ExpressibleByArrayLiteral {
let array: [M.Element]
init(array: [M.Element]) {
self.array = array
}
init(arrayLiteral elements: M.Element...) {
self.array = elements
}
struct ElementKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [M.Element] = []
while !container.isAtEnd {
let nested = try container.nestedContainer(keyedBy: ElementKey.self)
guard let key = nested.allKeys.first else { continue }
let metatype = M(rawValue: key.stringValue)
let superDecoder = try nested.superDecoder(forKey: key)
let object = try metatype?.type.init(from: superDecoder)
if let element = object as? M.Element {
elements.append(element)
}
}
array = elements
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try array.forEach { object in
var nested = container.nestedContainer(keyedBy: ElementKey.self)
let metatype = M.metatype(for: object)
if let key = ElementKey(stringValue: metatype.rawValue) {
let superEncoder = nested.superEncoder(forKey: key)
let encodable = object as? Encodable
try encodable?.encode(to: superEncoder)
}
}
}
}
Drawn from the accepted answer, I ended up with the following code that can be pasted into an Xcode Playground. I used this base to add a codable protocol to my app.
The output looks like this, without the nesting mentioned in the accepted answer.
ORIGINAL:
▿ __lldb_expr_33.Parent
- title: "Parent Struct"
▿ items: 2 elements
▿ __lldb_expr_33.NumberItem
- commonProtocolString: "common string from protocol"
- numberUniqueToThisStruct: 42
▿ __lldb_expr_33.StringItem
- commonProtocolString: "protocol member string"
- stringUniqueToThisStruct: "a random string"
ENCODED TO JSON:
{
"title" : "Parent Struct",
"items" : [
{
"type" : "numberItem",
"numberUniqueToThisStruct" : 42,
"commonProtocolString" : "common string from protocol"
},
{
"type" : "stringItem",
"stringUniqueToThisStruct" : "a random string",
"commonProtocolString" : "protocol member string"
}
]
}
DECODED FROM JSON:
▿ __lldb_expr_33.Parent
- title: "Parent Struct"
▿ items: 2 elements
▿ __lldb_expr_33.NumberItem
- commonProtocolString: "common string from protocol"
- numberUniqueToThisStruct: 42
▿ __lldb_expr_33.StringItem
- commonProtocolString: "protocol member string"
- stringUniqueToThisStruct: "a random string"
Paste into your Xcode project or Playground and customize to your liking:
import Foundation
struct Parent: Codable {
let title: String
let items: [Item]
init(title: String, items: [Item]) {
self.title = title
self.items = items
}
enum CodingKeys: String, CodingKey {
case title
case items
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(title, forKey: .title)
try container.encode(items.map({ AnyItem($0) }), forKey: .items)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
items = try container.decode([AnyItem].self, forKey: .items).map { $0.item }
}
}
protocol Item: Codable {
static var type: ItemType { get }
var commonProtocolString: String { get }
}
enum ItemType: String, Codable {
case numberItem
case stringItem
var metatype: Item.Type {
switch self {
case .numberItem: return NumberItem.self
case .stringItem: return StringItem.self
}
}
}
struct NumberItem: Item {
static var type = ItemType.numberItem
let commonProtocolString = "common string from protocol"
let numberUniqueToThisStruct = 42
}
struct StringItem: Item {
static var type = ItemType.stringItem
let commonProtocolString = "protocol member string"
let stringUniqueToThisStruct = "a random string"
}
struct AnyItem: Codable {
var item: Item
init(_ item: Item) {
self.item = item
}
private enum CodingKeys : CodingKey {
case type
case item
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(type(of: item).type, forKey: .type)
try item.encode(to: encoder)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(ItemType.self, forKey: .type)
self.item = try type.metatype.init(from: decoder)
}
}
func testCodableProtocol() {
var items = [Item]()
items.append(NumberItem())
items.append(StringItem())
let parent = Parent(title: "Parent Struct", items: items)
print("ORIGINAL:")
dump(parent)
print("")
let jsonEncoder = JSONEncoder()
jsonEncoder.outputFormatting = .prettyPrinted
let jsonData = try! jsonEncoder.encode(parent)
let jsonString = String(data: jsonData, encoding: .utf8)!
print("ENCODED TO JSON:")
print(jsonString)
print("")
let jsonDecoder = JSONDecoder()
let decoded = try! jsonDecoder.decode(type(of: parent), from: jsonData)
print("DECODED FROM JSON:")
dump(decoded)
print("")
}
testCodableProtocol()
Why wouldn't you use enums for the type of the tag?
struct Tag: Codable {
let type: TagType
let value: String
enum TagType: String, Codable {
case author
case genre
}
}
Then you can encode like try? JSONEncoder().encode(tag) or decode like let tags = try? JSONDecoder().decode([Tag].self, from: jsonData) and do any sort of processing as filtering the tags by type. You can do the same for the Article struct as well:
struct Tag: Codable {
let type: TagType
let value: String
enum TagType: String, Codable {
case author
case genre
}
}
struct Article: Codable {
let tags: [Tag]
let title: String
enum CodingKeys: String, CodingKey {
case tags
case title
}
}
I took the accepted answer from #Hamish, which is excellent, and generalized it a bit. Maybe useful to others, so posting it here...
First, setup reusable types similar to AnyTag and TagType.
protocol ConcreteTypeID: Codable {
var concreteType: any CodableExistential.Type { get }
}
protocol CodableExistential: Codable {
associatedtype TypeID: ConcreteTypeID
var concreteTypeId: TypeID { get }
}
struct ExistentialBox<TypeID: ConcreteTypeID>: Codable {
var existential: any CodableExistential
private enum CodingKey: Swift.CodingKey {
case concreteTypeId
}
init(_ existential: any CodableExistential) {
self.existential = existential
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKey.self)
let type = try container.decode(TypeID.self, forKey: .concreteTypeId)
self.existential = try type.concreteType.init(from: decoder)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKey.self)
try container.encode(existential.concreteTypeId, forKey: .concreteTypeId)
try existential.encode(to: encoder)
}
}
Now have your concrete types make use of these.
protocol Vehicle: CodableExistential {
var maker: String { get }
}
struct Car: Vehicle {
var concreteTypeId: VehicleTypeID { .car }
var maker: String
var numberOfPassengers: Int
}
struct Truck: Vehicle {
var concreteTypeId: VehicleTypeID { .truck }
var maker: String
}
enum VehicleTypeID: ConcreteTypeID {
case car, truck
var concreteType: any CodableExistential.Type {
switch self {
case .car:
return Car.self
case .truck:
return Truck.self
}
}
}
Lastly, encode/decode your types.
struct Fleet: Codable {
var vehicles: [any Vehicle]
enum CodingKey: Swift.CodingKey { case vehicles }
init(vehicles: [any Vehicle]) {
self.vehicles = vehicles
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKey.self)
let boxes = try container.decode([ExistentialBox<VehicleTypeID>].self, forKey: .vehicles)
vehicles = boxes.map { $0.existential as! any Vehicle }
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKey.self)
let boxes = vehicles.map { ExistentialBox<VehicleTypeID>($0) }
try container.encode(boxes, forKey: .vehicles)
}
}
let fleet = Fleet(vehicles: [Car(maker: "Toyota", numberOfPassengers: 2), Truck(maker: "Mack")])
let data = try JSONEncoder().encode(fleet)
let unpackedFleet = try JSONDecoder().decode(Fleet.self, from: data)
I'm not super happy with the cast used in the decode method of Fleet, but attempts to avoid that by changing the generics were met with classic errors like "any Vehicle cannot conform to Vehicle". If someone can find a better way, would love to hear it.