Skip to content

Commit 66083f6

Browse files
authored
Merge pull request swiftlang#16238 from mortenbekditlevsen/jsoncodingkeys
[WIP] JSON Coding Keys - string keyed dictionary opt out
2 parents 7355492 + 7076327 commit 66083f6

File tree

2 files changed

+211
-12
lines changed

2 files changed

+211
-12
lines changed

stdlib/public/SDK/Foundation/JSONEncoder.swift

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,42 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
/// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary`
14+
/// containing `Encodable` values (in which case it should be exempt from key conversion strategies).
15+
///
16+
/// NOTE: The architecture and environment check is due to a bug in the current (2018-08-08) Swift 4.2
17+
/// runtime when running on i386 simulator. The issue is tracked in https://bugs.swift.org/browse/SR-8276
18+
/// Making the protocol `internal` instead of `fileprivate` works around this issue.
19+
/// Once SR-8276 is fixed, this check can be removed and the protocol always be made fileprivate.
20+
#if arch(i386) && targetEnvironment(simulator)
21+
internal protocol _JSONStringDictionaryEncodableMarker { }
22+
#else
23+
fileprivate protocol _JSONStringDictionaryEncodableMarker { }
24+
#endif
25+
26+
extension Dictionary : _JSONStringDictionaryEncodableMarker where Key == String, Value: Encodable { }
27+
28+
/// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary`
29+
/// containing `Decodable` values (in which case it should be exempt from key conversion strategies).
30+
///
31+
/// The marker protocol also provides access to the type of the `Decodable` values,
32+
/// which is needed for the implementation of the key conversion strategy exemption.
33+
///
34+
/// NOTE: Please see comment above regarding SR-8276
35+
#if arch(i386) && targetEnvironment(simulator)
36+
internal protocol _JSONStringDictionaryDecodableMarker {
37+
static var elementType: Decodable.Type { get }
38+
}
39+
#else
40+
fileprivate protocol _JSONStringDictionaryDecodableMarker {
41+
static var elementType: Decodable.Type { get }
42+
}
43+
#endif
44+
45+
extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, Value: Decodable {
46+
static var elementType: Decodable.Type { return Value.self }
47+
}
48+
1349
//===----------------------------------------------------------------------===//
1450
// JSON Encoder
1551
//===----------------------------------------------------------------------===//
@@ -815,24 +851,55 @@ extension _JSONEncoder {
815851
}
816852
}
817853

818-
fileprivate func box<T : Encodable>(_ value: T) throws -> NSObject {
854+
fileprivate func box(_ dict: [String : Encodable]) throws -> NSObject? {
855+
let depth = self.storage.count
856+
let result = self.storage.pushKeyedContainer()
857+
do {
858+
for (key, value) in dict {
859+
self.codingPath.append(_JSONKey(stringValue: key, intValue: nil))
860+
defer { self.codingPath.removeLast() }
861+
result[key] = try box(value)
862+
}
863+
} catch {
864+
// If the value pushed a container before throwing, pop it back off to restore state.
865+
if self.storage.count > depth {
866+
let _ = self.storage.popContainer()
867+
}
868+
869+
throw error
870+
}
871+
872+
// The top container should be a new container.
873+
guard self.storage.count > depth else {
874+
return nil
875+
}
876+
877+
return self.storage.popContainer()
878+
}
879+
880+
fileprivate func box(_ value: Encodable) throws -> NSObject {
819881
return try self.box_(value) ?? NSDictionary()
820882
}
821883

822884
// This method is called "box_" instead of "box" to disambiguate it from the overloads. Because the return type here is different from all of the "box" overloads (and is more general), any "box" calls in here would call back into "box" recursively instead of calling the appropriate overload, which is not what we want.
823-
fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
824-
if T.self == Date.self || T.self == NSDate.self {
885+
fileprivate func box_(_ value: Encodable) throws -> NSObject? {
886+
// Disambiguation between variable and function is required due to
887+
// issue tracked at: https://bugs.swift.org/browse/SR-1846
888+
let type = Swift.type(of: value)
889+
if type == Date.self || type == NSDate.self {
825890
// Respect Date encoding strategy
826891
return try self.box((value as! Date))
827-
} else if T.self == Data.self || T.self == NSData.self {
892+
} else if type == Data.self || type == NSData.self {
828893
// Respect Data encoding strategy
829894
return try self.box((value as! Data))
830-
} else if T.self == URL.self || T.self == NSURL.self {
895+
} else if type == URL.self || type == NSURL.self {
831896
// Encode URLs as single strings.
832897
return self.box((value as! URL).absoluteString)
833-
} else if T.self == Decimal.self || T.self == NSDecimalNumber.self {
898+
} else if type == Decimal.self || type == NSDecimalNumber.self {
834899
// JSONSerialization can natively handle NSDecimalNumber.
835900
return (value as! NSDecimalNumber)
901+
} else if value is _JSONStringDictionaryEncodableMarker {
902+
return try self.box(value as! [String : Encodable])
836903
}
837904

838905
// The value should request a container from the _JSONEncoder.
@@ -2359,11 +2426,34 @@ extension _JSONDecoder {
23592426
}
23602427
}
23612428

2429+
fileprivate func unbox<T>(_ value: Any, as type: _JSONStringDictionaryDecodableMarker.Type) throws -> T? {
2430+
guard !(value is NSNull) else { return nil }
2431+
2432+
var result = [String : Any]()
2433+
guard let dict = value as? NSDictionary else {
2434+
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
2435+
}
2436+
let elementType = type.elementType
2437+
for (key, value) in dict {
2438+
let key = key as! String
2439+
self.codingPath.append(_JSONKey(stringValue: key, intValue: nil))
2440+
defer { self.codingPath.removeLast() }
2441+
2442+
result[key] = try unbox_(value, as: elementType)
2443+
}
2444+
2445+
return result as? T
2446+
}
2447+
23622448
fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
2449+
return try unbox_(value, as: type) as? T
2450+
}
2451+
2452+
fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
23632453
if type == Date.self || type == NSDate.self {
2364-
return try self.unbox(value, as: Date.self) as? T
2454+
return try self.unbox(value, as: Date.self)
23652455
} else if type == Data.self || type == NSData.self {
2366-
return try self.unbox(value, as: Data.self) as? T
2456+
return try self.unbox(value, as: Data.self)
23672457
} else if type == URL.self || type == NSURL.self {
23682458
guard let urlString = try self.unbox(value, as: String.self) else {
23692459
return nil
@@ -2373,10 +2463,11 @@ extension _JSONDecoder {
23732463
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
23742464
debugDescription: "Invalid URL string."))
23752465
}
2376-
2377-
return (url as! T)
2466+
return url
23782467
} else if type == Decimal.self || type == NSDecimalNumber.self {
2379-
return try self.unbox(value, as: Decimal.self) as? T
2468+
return try self.unbox(value, as: Decimal.self)
2469+
} else if let stringKeyedDictType = type as? _JSONStringDictionaryDecodableMarker.Type {
2470+
return try self.unbox(value, as: stringKeyedDictType)
23802471
} else {
23812472
self.storage.push(container: value)
23822473
defer { self.storage.popContainer() }

test/stdlib/TestJSONEncoder.swift

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,61 @@ class TestJSONEncoder : TestJSONEncoderSuper {
479479

480480
expectEqual(expected, resultString)
481481
}
482-
482+
483+
func testEncodingDictionaryStringKeyConversionUntouched() {
484+
let expected = "{\"leaveMeAlone\":\"test\"}"
485+
let toEncode: [String: String] = ["leaveMeAlone": "test"]
486+
487+
let encoder = JSONEncoder()
488+
encoder.keyEncodingStrategy = .convertToSnakeCase
489+
let resultData = try! encoder.encode(toEncode)
490+
let resultString = String(bytes: resultData, encoding: .utf8)
491+
492+
expectEqual(expected, resultString)
493+
}
494+
495+
private struct EncodeFailure : Encodable {
496+
var someValue: Double
497+
}
498+
499+
private struct EncodeFailureNested : Encodable {
500+
var nestedValue: EncodeFailure
501+
}
502+
503+
func testEncodingDictionaryFailureKeyPath() {
504+
let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)]
505+
506+
let encoder = JSONEncoder()
507+
encoder.keyEncodingStrategy = .convertToSnakeCase
508+
do {
509+
_ = try encoder.encode(toEncode)
510+
} catch EncodingError.invalidValue(let (_, context)) {
511+
expectEqual(2, context.codingPath.count)
512+
expectEqual("key", context.codingPath[0].stringValue)
513+
expectEqual("someValue", context.codingPath[1].stringValue)
514+
} catch {
515+
expectUnreachable("Unexpected error: \(String(describing: error))")
516+
}
517+
}
518+
519+
func testEncodingDictionaryFailureKeyPathNested() {
520+
let toEncode: [String: [String: EncodeFailureNested]] = ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]]
521+
522+
let encoder = JSONEncoder()
523+
encoder.keyEncodingStrategy = .convertToSnakeCase
524+
do {
525+
_ = try encoder.encode(toEncode)
526+
} catch EncodingError.invalidValue(let (_, context)) {
527+
expectEqual(4, context.codingPath.count)
528+
expectEqual("key", context.codingPath[0].stringValue)
529+
expectEqual("sub_key", context.codingPath[1].stringValue)
530+
expectEqual("nestedValue", context.codingPath[2].stringValue)
531+
expectEqual("someValue", context.codingPath[3].stringValue)
532+
} catch {
533+
expectUnreachable("Unexpected error: \(String(describing: error))")
534+
}
535+
}
536+
483537
private struct EncodeNested : Encodable {
484538
let nestedValue: EncodeMe
485539
}
@@ -606,6 +660,54 @@ class TestJSONEncoder : TestJSONEncoderSuper {
606660

607661
expectEqual("test", result.hello)
608662
}
663+
664+
func testDecodingDictionaryStringKeyConversionUntouched() {
665+
let input = "{\"leave_me_alone\":\"test\"}".data(using: .utf8)!
666+
let decoder = JSONDecoder()
667+
decoder.keyDecodingStrategy = .convertFromSnakeCase
668+
let result = try! decoder.decode([String: String].self, from: input)
669+
670+
expectEqual(["leave_me_alone": "test"], result)
671+
}
672+
673+
func testDecodingDictionaryFailureKeyPath() {
674+
let input = "{\"leave_me_alone\":\"test\"}".data(using: .utf8)!
675+
let decoder = JSONDecoder()
676+
decoder.keyDecodingStrategy = .convertFromSnakeCase
677+
do {
678+
_ = try decoder.decode([String: Int].self, from: input)
679+
} catch DecodingError.typeMismatch(let (_, context)) {
680+
expectEqual(1, context.codingPath.count)
681+
expectEqual("leave_me_alone", context.codingPath[0].stringValue)
682+
} catch {
683+
expectUnreachable("Unexpected error: \(String(describing: error))")
684+
}
685+
}
686+
687+
private struct DecodeFailure : Decodable {
688+
var intValue: Int
689+
}
690+
691+
private struct DecodeFailureNested : Decodable {
692+
var nestedValue: DecodeFailure
693+
}
694+
695+
func testDecodingDictionaryFailureKeyPathNested() {
696+
let input = "{\"top_level\": {\"sub_level\": {\"nested_value\": {\"int_value\": \"not_an_int\"}}}}".data(using: .utf8)!
697+
let decoder = JSONDecoder()
698+
decoder.keyDecodingStrategy = .convertFromSnakeCase
699+
do {
700+
_ = try decoder.decode([String: [String : DecodeFailureNested]].self, from: input)
701+
} catch DecodingError.typeMismatch(let (_, context)) {
702+
expectEqual(4, context.codingPath.count)
703+
expectEqual("top_level", context.codingPath[0].stringValue)
704+
expectEqual("sub_level", context.codingPath[1].stringValue)
705+
expectEqual("nestedValue", context.codingPath[2].stringValue)
706+
expectEqual("intValue", context.codingPath[3].stringValue)
707+
} catch {
708+
expectUnreachable("Unexpected error: \(String(describing: error))")
709+
}
710+
}
609711

610712
private struct DecodeMe3 : Codable {
611713
var thisIsCamelCase : String
@@ -1553,9 +1655,11 @@ JSONEncoderTests.test("testEncodingNonConformingFloats") { TestJSONEncoder().tes
15531655
JSONEncoderTests.test("testEncodingNonConformingFloatStrings") { TestJSONEncoder().testEncodingNonConformingFloatStrings() }
15541656
JSONEncoderTests.test("testEncodingKeyStrategySnake") { TestJSONEncoder().testEncodingKeyStrategySnake() }
15551657
JSONEncoderTests.test("testEncodingKeyStrategyCustom") { TestJSONEncoder().testEncodingKeyStrategyCustom() }
1658+
JSONEncoderTests.test("testEncodingDictionaryStringKeyConversionUntouched") { TestJSONEncoder().testEncodingDictionaryStringKeyConversionUntouched() }
15561659
JSONEncoderTests.test("testEncodingKeyStrategyPath") { TestJSONEncoder().testEncodingKeyStrategyPath() }
15571660
JSONEncoderTests.test("testDecodingKeyStrategyCamel") { TestJSONEncoder().testDecodingKeyStrategyCamel() }
15581661
JSONEncoderTests.test("testDecodingKeyStrategyCustom") { TestJSONEncoder().testDecodingKeyStrategyCustom() }
1662+
JSONEncoderTests.test("testDecodingDictionaryStringKeyConversionUntouched") { TestJSONEncoder().testDecodingDictionaryStringKeyConversionUntouched() }
15591663
JSONEncoderTests.test("testEncodingKeyStrategySnakeGenerated") { TestJSONEncoder().testEncodingKeyStrategySnakeGenerated() }
15601664
JSONEncoderTests.test("testDecodingKeyStrategyCamelGenerated") { TestJSONEncoder().testDecodingKeyStrategyCamelGenerated() }
15611665
JSONEncoderTests.test("testKeyStrategySnakeGeneratedAndCustom") { TestJSONEncoder().testKeyStrategySnakeGeneratedAndCustom() }
@@ -1572,5 +1676,9 @@ JSONEncoderTests.test("testEncoderStateThrowOnEncodeCustomData") { TestJSONEncod
15721676
JSONEncoderTests.test("testDecoderStateThrowOnDecode") { TestJSONEncoder().testDecoderStateThrowOnDecode() }
15731677
JSONEncoderTests.test("testDecoderStateThrowOnDecodeCustomDate") { TestJSONEncoder().testDecoderStateThrowOnDecodeCustomDate() }
15741678
JSONEncoderTests.test("testDecoderStateThrowOnDecodeCustomData") { TestJSONEncoder().testDecoderStateThrowOnDecodeCustomData() }
1679+
JSONEncoderTests.test("testEncodingDictionaryFailureKeyPath") { TestJSONEncoder().testEncodingDictionaryFailureKeyPath() }
1680+
JSONEncoderTests.test("testEncodingDictionaryFailureKeyPathNested") { TestJSONEncoder().testEncodingDictionaryFailureKeyPathNested() }
1681+
JSONEncoderTests.test("testDecodingDictionaryFailureKeyPath") { TestJSONEncoder().testDecodingDictionaryFailureKeyPath() }
1682+
JSONEncoderTests.test("testDecodingDictionaryFailureKeyPathNested") { TestJSONEncoder().testDecodingDictionaryFailureKeyPathNested() }
15751683
runAllTests()
15761684
#endif

0 commit comments

Comments
 (0)