Skip to content

Instantly share code, notes, and snippets.

@artyom-stv
Last active August 30, 2022 18:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save artyom-stv/848b4416cf734ae559e44f6c752a2100 to your computer and use it in GitHub Desktop.
Save artyom-stv/848b4416cf734ae559e44f6c752a2100 to your computer and use it in GitHub Desktop.
JSON Pointer support for `JSONDecoder` (see https://www.rfc-editor.org/rfc/rfc6901)
let json = #"{"a":123,"b":[456,{"x":"foo"}]}"#
let data = json.data(using: .utf8)!
// Prints `123`
print(try JSONDecoder().decode(Int.self, from: data, atPointer: "/a"))
// Prints `foo`
print(try JSONDecoder().decode(String.self, from: data, atPointer: "/b/1/x"))
public extension JSONDecoder {
struct CodingPathComponent {
private enum Storage {
case int(Int)
case string(String)
}
var intValue: Int? {
switch storage {
case let .int(value):
return value
case let .string(value):
return Int(value)
}
}
var stringValue: String {
switch storage {
case let .int(value):
return String(value)
case let .string(value):
return value
}
}
private let storage: Storage
public init(_ intValue: Int) {
self.storage = .int(intValue)
}
public init(_ stringValue: String) {
self.storage = .string(stringValue)
}
}
/// Returns a value of the specified type, decoded from a JSON object at the specified path.
///
/// - Parameters:
/// - type: The type of the value to decode from the supplied JSON object.
/// - data: The JSON object to decode.
/// - codingPath: The path to the target value.
func decode<T, CodingPath>(
_ type: T.Type,
from data: Data,
at codingPath: CodingPath
) throws -> T where T: Decodable, CodingPath: Sequence, CodingPath.Element == CodingPathComponent {
let oldValue = userInfo[_CodingPathLookup.codingPathUserInfoKey]
userInfo[_CodingPathLookup.codingPathUserInfoKey] = AnySequence(codingPath)
defer { userInfo[_CodingPathLookup.codingPathUserInfoKey] = oldValue }
return try decode(_CodingPathLookup.Container<T>.self, from: data).value
}
/// Returns a value of the specified type, decoded from a JSON object at the specified
/// [JSON pointer](https://www.rfc-editor.org/rfc/rfc6901).
///
/// - Parameters:
/// - type: The type of the value to decode from the supplied JSON object.
/// - data: The JSON object to decode.
/// - jsonPointer: The JSON pointer to the target value.
///
/// - Note: The supplied JSON pointer is not validated.
///
/// For example, given the JSON document
///
/// {
/// "a": 123,
/// "b": [
/// 456,
/// {
/// "x": "foo"
/// }
/// ]
/// }
///
/// "/a" points to 123.
/// "/b/1/x" points "foo".
func decode<T>(_ type: T.Type, from data: Data, atPointer jsonPointer: String) throws -> T where T: Decodable {
let codingPath = jsonPointer
.split(separator: "/", omittingEmptySubsequences: true)
.lazy
.map { token -> CodingPathComponent in
let token = token
.replacingOccurrences(of: "~1", with: "/")
.replacingOccurrences(of: "~0", with: "~")
if let intValue = Int(token) {
return CodingPathComponent(intValue)
} else {
return CodingPathComponent(String(token))
}
}
return try decode(type, from: data, at: codingPath)
}
}
extension JSONDecoder.CodingPathComponent: CustomStringConvertible {
public var description: String {
stringValue
}
}
extension JSONDecoder.CodingPathComponent: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
}
extension JSONDecoder.CodingPathComponent: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int) {
self.init(value)
}
}
private enum _CodingPathLookup {
struct Container<Value>: Decodable where Value: Decodable {
private struct CodingKeys: CodingKey {
let stringValue: String
var intValue: Int? { nil }
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
nil
}
init(_ stringValue: String) {
self.stringValue = stringValue
}
}
private struct AnyDecodableValue: Decodable {}
let value: Value
init(from decoder: Decoder) throws {
guard
let codingPath = decoder.userInfo[codingPathUserInfoKey] as? AnySequence<JSONDecoder.CodingPathComponent>
else {
throw LookupError.userInfoKeyNotFound
}
var decoder = decoder
for component in codingPath {
if var container = try? decoder.unkeyedContainer() {
if let intValue = component.intValue {
while container.currentIndex < intValue {
// Ignore array element.
_ = try? container.decode(AnyDecodableValue.self)
}
decoder = try container.superDecoder()
continue
}
// If the component doesn't have an integer value, we don't `continue`, because we want
// the `container(keyedBy:)` method to throw `DecodingError.typeMismatch`.
}
decoder = try decoder
.container(keyedBy: CodingKeys.self)
.superDecoder(forKey: CodingKeys(component.stringValue))
}
self.value = try Value(from: decoder)
}
}
enum LookupError: Error {
case userInfoKeyNotFound
}
static let codingPathUserInfoKey = CodingUserInfoKey(rawValue: "initialCodingPath")!
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment