Decoding JSON with Dynamic Types in Swift [With Codable]

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.



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 GUIDE

Table 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 GUIDE

Leave a Comment