Codable Dynamic Types

I'd like to use the Codable type to decode a JSON "object" whose available properties are dynamically driven by a provided "type" value. Here's a simplified example for various animals:

{
  "type": "bird",
  "properties": {
    "feathers": "soft",
    "beakLength": "short"
  }
}

Another variation would be:

{
  "type": "dog",
  "properties": {
    "bark": "loud",
    "paws": "small"
  }
}

From what I can tell, there is not any type of standard Swift object/struct/enum modeling that would easily support dynamic properties like this. The swift enum with associated values seems promising but the case name becomes a key at the top level like so:

enum Animal: Codable {
  case bird(feathers: String, beakLength: String)
  case dog(bark: String, paws: String)
}

{
  "bird": {
    "feathers": "soft",
    "beakLength":"short"
  }
}

As my JSON examples at the beginning show, the key in the JSON is "properties" for all types, not "bird' as results when decoding an enum. I'd like Swift to reference the "type" property and decode the case in the "properties" property which I suppose is not possible.

The way I've worked around this is by creating a "super" structure with all possible properties across various types and make all them them optional. I then need to do special validation to make sure the properties I expect are there based on the type. This is not ideal as I'd prefer to rely on Swift's type system to do this for me. Are there any other obvious options I've missed?

struct Properties {
    let feathers: String?
    let beakLength: String?
    let bark: String?
    let paws: String?
}

Your properties aren't what I'd really call "dynamically driven", because — given the type — you apparently know what they are in advance, otherwise your workaround wouldn't be possible.

When your JSON is structured this way, you can write your own Decodable conformance. In your custom init, grab the keyed container and decode just the "type" key. Let that guide you as to what else to decode afterwards, from the same container.


If you don't like that approach, the you can always use JSONSerialization, which decodes the entire JSON source into a Dictionary with nested arrays and dictionaries. You can then traverse the contents to validate and extract the content you expect.

1 Like

I was in the middle of writing the following when this thread disappeared for a little bit, but to add to what @QuinceyMorris says:

The easiest way to handle this in a way that translates to "neat" types in Swift will be to implement init(from:) manually, inspecting the type value and switching over that to pull values out of properties as needed (you can use a nested container to reach into the data hierarchy, without creating an intermediate Properties type). You can use this approach to map to several different data representations in Swift, but to use your enum Animal as an example:

enum Animal: Decodable {
    case bird(feathers: String, beakLength: String)
    case dog(bark: String, paws: String)
    
    private enum CodingKeys: String, CodingKey {
        case type, properties
    }
    
    private enum PropertyCodingKeys: String, CodingKey {
        case feathers, beakLength, bark, paws
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let properties = try container.nestedContainer(keyedBy: PropertyCodingKeys.self, forKey: .properties)
        
        let type = try container.decode(String.self, forKey: .type)
        switch type {
        case "bird":
            self = .bird(
                feathers: try properties.decode(String.self, forKey: .feathers),
                beakLength: try properties.decode(String.self, forKey: .beakLength)
            )
            
        case "dog":
            self = .dog(
                bark: try properties.decode(String.self, forKey: .bark),
                paws: try properties.decode(String.self, forKey: .paws)
            )
            
        default:
            throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unrecognized Animal type \(type)")
        }
    }
}

Then, given the data you've laid out, you can get the results you expect:

import Foundation

let json = """
{
    "type": "dog",
    "properties": {
        "bark": "loud",
        "paws": "small"
    }
}
""".data(using: .utf8)!

let animal = try JSONDecoder().decode(Animal.self, from: json)
print(animal) // => dog(bark: "loud", paws: "small")
6 Likes

Yeah "dynamic" was not a great description given the context. I meant to say the same value for the properties may map to different objects. But the variations of objects are known in advance.

The JSONSerialization approach is something to consider. I had considered whether this JSON design was some type of anti-pattern, given that it does not cleanly map to Swift Codable paradigms. I've come to believe that is not the case though -- I've seen patterns like my example modeled this way from several reputable sources.

I really appreciate the code examples and will pursue a similar approach as you've outlined. I like that this approach hides this logic in the decoder initializer and allows for the clients to not need to worry about using special adhoc class methods.

1 Like

You shouldn't ever have to use JSONSerialization directly when writing a custom Decodable initializer.

1 Like

Yeah, there's really no value to JSONSerialization here. You do more work getting your values out than just adopting a Codable JSON type which you can use. But really Codable can do this just fine. I suggest moving your associated values to their own types rather than using tuples / separate associated properties, but it works just fine.

1 Like

One approach might be something like what is shown in this blog post. I don't vouch for it, haven't used it, just passing along.

2 Likes