Skip to content

Instantly share code, notes, and snippets.

@shawnthroop
Last active April 21, 2025 10:16
Show Gist options
  • Save shawnthroop/e2da452b31cab830f02694da0ab6bad6 to your computer and use it in GitHub Desktop.
Save shawnthroop/e2da452b31cab830f02694da0ab6bad6 to your computer and use it in GitHub Desktop.
JSON expressed as an Codable enum
public enum JSON : Sendable {
case null
case value(any Codable & Sendable)
case list([JSON])
case object([String: JSON])
}
extension JSON : Codable, CustomStringConvertible {
public func encode(to encoder: any Encoder) throws {
switch self {
case .null:
var container = encoder.singleValueContainer()
try container.encodeNil()
case .value(let value):
var container = encoder.singleValueContainer()
try container.encode(value)
case .list(let values):
var container = encoder.unkeyedContainer()
for value in values {
try container.encode(value)
}
case .object(let object):
var container = encoder.container(keyedBy: AnyCodingKey.self)
for (key, value) in object {
try container.encode(value, forKey: .string(key))
}
}
}
public init(from decoder: any Decoder) throws {
if let container = try ignoringTypeMismatch(decoder.container(keyedBy: AnyCodingKey.self)) {
try self.init(container: container)
} else if var container = try ignoringTypeMismatch(decoder.unkeyedContainer()) {
try self.init(container: &container)
} else {
try self.init(container: decoder.singleValueContainer())
}
}
public var description: String {
switch self {
case .null:
"null"
case .value(let value):
"\(value)"
case .list(let values):
values.description
case .object(let content):
content.description
}
}
}
public enum AnyCodingKey : Hashable, Sendable {
case string(String)
case int(Int)
}
extension AnyCodingKey: CodingKey {
public var stringValue: String {
switch self {
case .string(let value):
value
case .int(let value):
String(value)
}
}
public var intValue: Int? {
switch self {
case .string:
nil
case .int(let value):
value
}
}
public init(intValue: Int) {
self = .int(intValue)
}
public init(stringValue: String) {
self = .string(stringValue)
}
}
private extension JSON {
init<Container: KeyedDecodingContainerProtocol>(container: Container) throws {
self = try .object(container.allKeys.reduce(into: [:]) { object, key in
object[key.stringValue] = try container.decode(JSON.self, forKey: key)
})
}
init(container: inout UnkeyedDecodingContainer) throws {
var values = [JSON]()
while !container.isAtEnd {
try values.append(container.decode(JSON.self))
}
self = .list(values)
}
init(container: SingleValueDecodingContainer) throws {
if container.decodeNil() {
self = .null
} else if let bool = try ignoringTypeMismatch(container.decode(Bool.self)) {
self = .value(bool)
} else if let int = try ignoringTypeMismatch(container.decode(Int.self)) {
self = .value(int)
} else if let double = try ignoringTypeMismatch(container.decode(Double.self)) {
self = .value(double)
} else if let string = try ignoringTypeMismatch(container.decode(String.self)) {
self = .value(string)
} else if let list = try ignoringTypeMismatch(container.decode([JSON].self)) {
self = .list(list)
} else if let object = try ignoringTypeMismatch(container.decode([String: JSON].self)) {
self = .object(object)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported type")
}
}
}
/// Perform body while catching and ignoring DecodingError.typeMismatch error.
func ignoringTypeMismatch<T>(_ body: @autoclosure () throws -> T) throws -> T? {
do {
return try body()
} catch DecodingError.typeMismatch {
return nil
} catch {
throw error
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment