Last active
August 30, 2022 18:17
-
-
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)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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