← Back to list

Representing arbitrary data (e.g JSON) as a custom and opaque Codable type

Represent arbitrary data (e.g JSON) using a custom and opaque Codable type consisting of a hierarchy of primitive data values and nested…

Thomas Asheim Smedmann · 2024-07-14 15:17 · 0 claps · 2.7 min read
#swift #codable #json #ios
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Representing arbitrary data (e.g JSON) as a custom and opaque Codable type

Represent arbitrary data (e.g JSON) using a custom and opaque Codable type consisting of a hierarchy of primitive data values and nested Codables.

Arbitrary data/JSON represented as a custom/opaque Codable type.

Arbitrary data/JSON represented as a custom/opaque Codable type.

Disclaimer: The idea and example code is heavily inspired by Rob Napier’s answer to this StackOverflow post (thank you!).

[embed]ios-examples/OpaqueValue at main · thomsmed/ios-examples A collection of iOS example code and applications. - ios-examples/OpaqueValue at main · thomsmed/ios-examplesgithub.com

The OpaqueValue type

Building upon Swift’s concept of Encodable/Decodable and Encoder/Decoder, we can define a type that can represent arbitrary (Encodable/Decodable) data. Basically making the data opaque to our app.

The secret lies in manually implementing Encodable.encode(to: _) and Decodable.init(from: _), and also defining a struct conforming to the CodingKey protocol. This allows us to not be explicit about property names.

/// An opaque Codable type that can represent arbitrary (Codable) data. E.g some arbitrary JSON.
/// It consists of one or more primitive types and/or one or more nested opaque values.
///
/// This code is heavily inspired by [Rob Napier](https://stackoverflow.com/users/97337/rob-napier)'s answer to this [StackOverflow post](https://stackoverflow.com/questions/65901928/swift-jsonencoder-encoding-class-containing-a-nested-raw-json-object-literal).
enum OpaqueValue: Equatable {
    struct PropertyKey: CodingKey, Hashable {
        var stringValue: String
        var intValue: Int?

        init?(stringValue: String) {
            self.stringValue = stringValue
        }

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

    case object([PropertyKey: OpaqueValue])
    case array([OpaqueValue])
    case string(String)
    case number(Double)
    case boolean(Bool)
    case null
}

// MARK: OpaqueValue+Encodable

extension OpaqueValue: Encodable {
    func encode(to encoder: any Encoder) throws {
        switch self {
            case .object(let values):
                var container = encoder.container(keyedBy: PropertyKey.self)
                for (key, value) in values {
                    try container.encode(value, forKey: key)
                }
            case .array(let values):
                var container = encoder.unkeyedContainer()
                for value in values {
                    try container.encode(value)
                }
            case .string(let value):
                var container = encoder.singleValueContainer()
                try container.encode(value)
            case .number(let value):
                var container = encoder.singleValueContainer()
                try container.encode(value)
            case .boolean(let value):
                var container = encoder.singleValueContainer()
                try container.encode(value)
            case .null:
                var container = encoder.singleValueContainer()
                try container.encodeNil()
        }
    }
}

// MARK: OpaqueValue+Decodable

extension OpaqueValue: Decodable {
    init(from decoder: any Decoder) throws {
        if let container = try? decoder.container(keyedBy: PropertyKey.self) {
            var values: [PropertyKey: OpaqueValue] = [:]
            for key in container.allKeys {
                values[key] = try container.decode(OpaqueValue.self, forKey: key)
            }
            self = .object(values)
        } else if var container = try? decoder.unkeyedContainer() {
            var values: [OpaqueValue] = []
            while !container.isAtEnd {
                values.append(try container.decode(OpaqueValue.self))
            }
            self = .array(values)
        } else {
            let container = try decoder.singleValueContainer()
            if let value = try? container.decode(String.self) {
                self = .string(value)
            } else if let value = try? container.decode(Double.self) {
                self = .number(value)
            } else if let value = try? container.decode(Bool.self) {
                self = .boolean(value)
            } else {
                guard container.decodeNil() else {
                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Data unrecognizable")
                }
                self = .null
            }
        }
    }
}

Representing arbitrary JSON as OpaqueValue

With our OpaqueValue type we can easily decode/encode arbitrary JSON.

let someArbitraryJSONData = Data("""
{
    "message": {
        "text": "Hello World"
    },
    "texts": ["Hello", "World"],
    "note": "Hello World",
    "age": 1337,
    "tooOld": true,
    "child": null
}
""".utf8)

var opaqueValue = try JSONDecoder().decode(OpaqueValue.self, from: someArbitraryJSONData)

let someOtherArbitraryJSONData = Data("""
{
    "user": {
        "name": "Thomas",
        "id": 1337,
        "isAdmin": false,
        "lastUpdated": null
    },
    "mix": ["Hello World", 1337, false, null],
}
""".utf8)

opaqueValue = try JSONDecoder().decode(OpaqueValue.self, from: someOtherArbitraryJSONData)

// ...

struct SomeDataModel: Encodable {
    let id: Int
    let text: String
    let opaqueData: OpaqueValue
}

let someDataModel = SomeDataModel(id: 1337, text: "Hello World", opaqueData: opaqueValue)

let someDataModelAsJSON = try JSONEncoder().encode(someDataModel)

Final words

Neat, right!? From time to time I find my self in a situation where I need to pass some arbitrary data from one back-end to another. And though I might need to move the data around in my app a bit before passing it along to the next back-end, my app doesn’t really need to know the details about the content of the data. And sometimes the data should be completely opaque to my app.

Then OpaqueValue is perfect!

[embed]ios-examples/OpaqueValue at main · thomsmed/ios-examples A collection of iOS example code and applications. - ios-examples/OpaqueValue at main · thomsmed/ios-examplesgithub.com

Happy coding! 😄


메타데이터
post_id
dfaa07b22cd3
slug
representing-arbitrary-data-e-g-json-as-a-custom-and-opaque-codable-type-dfaa07b22cd3
url
https://medium.com/@thomsmed/representing-arbitrary-data-e-g-json-as-a-custom-and-opaque-codable-type-dfaa07b22cd3
canonical_url
https://medium.com/@thomsmed/representing-arbitrary-data-e-g-json-as-a-custom-and-opaque-codable-type-dfaa07b22cd3
author_url
https://medium.com/@thomsmed
status
ok
fetched_at
2026-07-23 09:39:55