JSON data is not always homogenous. There are instances in which a JSON object field can contain objects containing different information.
In such cases, the Swift type for the decoding must be determined dynamically.
- This article is part of the Parsing JSON in Swift series.
FREE GUIDE: Parsing JSON in Swift - The Ultimate Cheat Sheet
Learn the fundamentals of JSON parsing in Swift, advanced decoding techniques, and tasks for app development
DOWNLOAD THE FREE GUIDETable of contents
Dynamic JSON objects usually carry their type information in a field
Working with dynamic types complicates decoding data in Swift using the Codable
protocols because a decoder must know the concrete Swift type to decode data.
However, when a type is dynamic, it’s impossible to know it before parsing the JSON data. We need to take extra steps to determine the correct type first.
For example, the following JSON data for a social media user contains an array of posts of different types.
{
"name": "John Doe",
"posts": [
{
"type": "text",
"value": {
"content": "Hello, World!"
}
},
{
"type": "image",
"value": {
"url": "http://example.com/images/photo.jpeg"
}
}
]
}
When a JSON object has a dynamic type, this is usually explicitly stated in a property of the object.
In the above JSON data, the post type is contained in the type
property, while the rest of the information is nested inside another object contained by the value
property.
Making concrete Swift types conform to the same protocol
As usual, we need concrete Swift types matching the data structure to parse nested JSON objects with Codable.
However, we must make such types somehow homogeneous since they must be contained inside an Array
that can only contain values of the same kind.
Inheritance is how we abstract different types in a statically typed language like Swift. All the types we want to homogenize must descend from a common superclass or conform to the same protocols.
protocol Post: Decodable {}
struct Text: Post {
let content: String
}
struct Image: Post {
let url: URL
}
struct User: Decodable {
let name: String
let posts: [Post]
}
Both our Text
and Image
post types conform to the Post
protocol, which allows us to store their values inside the posts
stored property of the User
type. The posts
property is a collection, but you would use the same approach for properties containing a single value.
You might find another solution in some online tutorials. Instead of using inheritance, JSON objects with a dynamic type are sometimes decoded using an enumeration with associated values.
This technique also allows different values to have the same type since all cases belong to the same Swift enum. However, it is not a good practice since it will fill your code with switch
or if case
statements.
It’s always better to avoid using enumerations in such cases because they can easily violate the open-closed principle.
Decoding a JSON object according to its dynamic type with a custom decoding initializer
We determine the dynamic type of the nested objects and decode them in a decoding initializer in Swift type for the parent JSON object.
First, we need coding keys for both the parent and nested types.
struct User: Decodable {
let name: String
let posts: [Post]
enum CodingKeys: CodingKey {
case name, posts
}
enum PostCodingKeys: CodingKey {
case type, value
}
}
As in our example, objects with dynamic types are often encoded inside an array. We can use an unkeyed decoding container to iterate over the array and a keyed container to read each JSON object’s type
property. Otherwise, you can use a keyed decoding container instead.
After reading the type
field in the JSON object, we can use that information to decode the object in the value
field using the appropriate Decodable
Swift type.
struct User: Decodable {
let name: String
let posts: [Post]
enum CodingKeys: CodingKey {
case name, posts
}
enum PostCodingKeys: CodingKey {
case type, value
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
var postsContainer = try container.nestedUnkeyedContainer(forKey: .posts)
var posts: [Post] = []
while !postsContainer.isAtEnd {
let postContainer = try postsContainer.nestedContainer(keyedBy: PostCodingKeys.self)
let type = try postContainer.decode(String.self, forKey: .type)
let post: Post = switch type {
case "text":
try postContainer.decode(Text.self, forKey: .value)
case "image":
try postContainer.decode(Image.self, forKey: .value)
default:
throw DecodingError.dataCorruptedError(
forKey: .value,
in: postContainer,
debugDescription: "Unknown post type"
)
}
posts.append(post)
}
self.posts = posts
}
}
If you want a quick reference of decoding JSON objects with dynamic types, together with other JSON parsing techniques, download my free cheat sheet below.
FREE GUIDE: Parsing JSON in Swift - The Ultimate Cheat Sheet
Learn the fundamentals of JSON parsing in Swift, advanced decoding techniques, and tasks for app development
DOWNLOAD THE FREE GUIDEMatteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.