DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Swift: Master of Decoding Messy JSON
  • How I Used Swift Script in Electron Browser Natively
  • What Is Pydantic?
  • Introduction to Couchbase for Oracle Developers and Experts: Part 4: Data Modeling

Trending

  • Hallucination Has Real Consequences — Lessons From Building AI Systems
  • The Art of Token Frugality in Generative AI Applications
  • Querying Without a Query Language
  • Swift Concurrency Part 4: Actors, Executors, and Reentrancy
  1. DZone
  2. Coding
  3. Languages
  4. Advanced Usage of Decodable in Swift: Handling Dynamic Keys

Advanced Usage of Decodable in Swift: Handling Dynamic Keys

Use DynamicKey to safely decode JSON with unpredictable keys — it avoids fragile if let chains and makes your decoding logic flexible and maintainable.

By 
Ada Kirsch user avatar
Ada Kirsch
·
Nov. 20, 25 · Analysis
Likes (1)
Comment
Save
Tweet
Share
1.3K Views

Join the DZone community and get the full member experience.

Join For Free

When your backend sends responses that don't follow a consistent structure, Swift's Decodable system can begin to reveal its limitations. It expects structure. Predictability. Stability. However, real-world APIs — especially those powering social feeds, content backends, or any CMS-driven application — rarely fit that mold.

This article takes a look under the hood of Swift's decoding system. The goal isn't to memorize recipes, but to understand what's really happening so you can build decoding logic that scales with the unpredictable nature of your APIs.

What Decodable Does

Decodable is Swift's bridge between untyped data and strongly-typed models. When you call JSONDecoder().decode, Swift walks your type's structure, looking for property names that match JSON keys as defined in your CodingKeys enum.

That's perfect when the shape of your JSON is known at compile time. But if the server starts swapping key names depending on content type — "article" today, "wiki" tomorrow — your decoding logic breaks.

The Problem: Dynamic Server Keys

Let's look at a feed response that changes shape depending on what the server sends:

JSON
 
{
"article": {
"title": "Exploring Swift Concurrency",
"itemTypeName": "blogArticle",
"author": "Jane Doe"
}
}


Then, in another case:

JSON
 
{
"wiki": {
"title": "Internal Guidelines",
"itemTypeName": "wiki",
"editor": "John Smith"
}
}

And sometimes, you get:

JSON
 
{
"message": "Server will go down for maintenance at midnight."
}


Each top-level key (article, wiki, message) signals a different content type. You can't predict this key at compile time, so a static CodingKeys enum won't help.

Handling Dynamic Keys

Brute Force: Conditional Decoding

A first attempt might look like this:

JSON
 
let container = try decoder.container(keyedBy: CodingKeys.self)

if let article = try? container.decode(SharedArticle.self, forKey: .article) {
self = .article(article: article)
} else if let wiki = try? container.decode(SharedArticle.self, forKey: .wiki) {
self = .wiki(article: wiki)
} else if let message = try? container.decode(String.self, forKey: .message) {
self = .post(message: message)
} else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown key"))
}


It works — until it doesn’t. Every new key type means another conditional branch. Maintenance hell.

A Better Way: The DynamicKey Pattern

To handle unknown keys gracefully, define a reusable CodingKey that can represent any key:

JSON
 
public struct DynamicKey: CodingKey {
public var intValue: Int? { nil }
    public var stringValue: String


public init?(intValue: Int) { nil }
public init?(stringValue: String) { self.stringValue = stringValue }
}


Now, you can write a single initializer that decodes dynamically:

JSON
 
public enum TimelinePostContent: Decodable {
case post(message: String)
case article(article: SharedArticle)
case wiki(article: SharedArticle)
case event(event: SharedEvent)
case page(page: SharedPage)
case workspace(workspace: SharedWorkspace)
case blocks(blocks: [SharedBlock])

private enum CodingKeys: String, CodingKey {
case message, article, event, page, workspace, blocks
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicKey.self)

for key in container.allKeys {
switch CodingKeys(rawValue: key.stringValue) {
case .message:
self = .post(message: try container.decode(String.self, forKey: key))
return
case .article:
let article = try container.decode(SharedArticle.self, forKey: key)
switch article.itemTypeName {
case .wiki:
self = .wiki(article: article)
case .blogArticle:
self = .article(article: article)
default:
throw DecodingError.dataCorruptedError(forKey: key, in: container, debugDescription: "Unexpected article type")
}
return
case .event:
self = .event(event: try container.decode(SharedEvent.self, forKey: key))
return
case .page:
self = .page(page: try container.decode(SharedPage.self, forKey: key))
return
case .workspace:
self = .workspace(workspace: try container.decode(SharedWorkspace.self, forKey: key))
return
case .blocks:
self = .blocks(blocks: try container.decode([SharedBlock].self, forKey: key))
return
default:
continue
}
}

throw DecodingError.dataCorruptedError(forKey: DynamicKey(stringValue: "data")!, in: container, debugDescription: "Failed to parse data container")
}
}


No hardcoded keys in CodingKeys. No repetitive if let jungle. You’re using runtime inspection while keeping type safety intact.

Why the DynamicKey Pattern Wins

Dynamic decoding isn't about being clever — it's about control. When your app interacts with APIs that change over time or vary by content type, you need a strategy that strikes a balance between resilience and clarity. The DynamicKey approach delivers both.

It Handles Unpredictable Data Gracefully

With DynamicKey, you're no longer hardcoding assumptions about what the server will send. Instead, you inspect what's actually there at runtime. That means fewer decoding crashes when the backend team adds a new type or wraps payloads differently. The code adapts without breaking — and when something unexpected appears, it fails in a predictable, debuggable way.

It Keeps You Out of the “Conditional Jungle”

If you've ever maintained a long list of if let article = try? ... else if let wiki = try? ..., you know how fragile it gets. Each new branch duplicates logic, and every edit invites subtle errors. The dynamic approach replaces that tangle with one tight loop that reads the keys and matches them to your known cases. You're not rewriting decoding logic — you're describing the data's intent once and extending it over time.

It Scales Without Losing Type Safety

A standard fallback for unpredictable JSON is to decode into [String: Any] or rely on manual parsing with JSONSerialization. That's flexible, but you pay for it in safety and maintainability. DynamicKey gives you the same flexibility without giving up static typing — your enums and structs remain checked by the compiler. It's dynamic where it needs to be and strongly typed everywhere else.

It Makes Change Manageable

When new content types arrive, you don't rewrite your decoding logic. You add one case, one decoding branch, and the system continues to function. This makes it easier to evolve your model layer without touching every endpoint or rewriting half your parsing logic. Over time, that predictability matters — especially in larger codebases or teams where multiple developers handle different features.

It Encourages Explicitness Over Magic

This isn't reflection, introspection, or some metaprogramming trick. The flow is explicit, visible, and debuggable. If decoding fails, you know exactly which key caused it and why. That transparency makes onboarding easier and debugging faster — two things you'll appreciate when you're diagnosing issues at 2 AM.

Trade-Offs Worth Understanding

  • No compile-time key validation: Swift won't warn you if the backend changes a key name. You'll catch it in tests, not builds.
  • More upfront code: The initializer is longer than a static one, but it's still centralized and predictable.
  • Testing becomes essential: Because flexibility means more possible paths, you need a solid suite of decoding tests that cover real and edge-case payloads.

In short, DynamicKey is a pragmatic pattern for addressing the complex aspects of networked data. It doesn't hide complexity — it gives you a structure to manage it.

Summary

  • Use CodingKeys when your schema is stable.
  • Use a DynamicKey helper when your API isn’t.
  • Keep decoding logic explicit — clarity beats cleverness.

The DynamicKey pattern is one of those small shifts that changes how you think about decoding: from rigid mappings to flexible, data-driven parsing. It's not a trick — it's an understanding of how Swift's decoding machinery works and how to make it serve the complex, dynamic world of real APIs.

JSON Swift (programming language) Data modeling

Opinions expressed by DZone contributors are their own.

Related

  • Swift: Master of Decoding Messy JSON
  • How I Used Swift Script in Electron Browser Natively
  • What Is Pydantic?
  • Introduction to Couchbase for Oracle Developers and Experts: Part 4: Data Modeling

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook