Skip to content

Commit 205e2cb

Browse files
author
Itai Ferber
committed
Ensure values are decodable from nil
* Eliminate null checks from unboxing in the general case (so types can at least attempt to decode from nil) * Add unit tests to confirm this behavior
1 parent ee64f4a commit 205e2cb

File tree

4 files changed

+142
-58
lines changed

4 files changed

+142
-58
lines changed

stdlib/public/SDK/Foundation/JSONEncoder.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,8 +1280,8 @@ fileprivate struct _JSONKeyedDecodingContainer<K : CodingKey> : KeyedDecodingCon
12801280
}
12811281

12821282
private func _superDecoder(forKey key: CodingKey) throws -> Decoder {
1283-
return try self.decoder.with(pushedKey: key) {
1284-
let value = self.container[key.stringValue]
1283+
return self.decoder.with(pushedKey: key) {
1284+
let value: Any = self.container[key.stringValue] ?? NSNull()
12851285
return _JSONDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options)
12861286
}
12871287
}
@@ -2069,13 +2069,13 @@ extension _JSONDecoder {
20692069
}
20702070

20712071
fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
2072-
guard !(value is NSNull) else { return nil }
2073-
20742072
let decoded: T
20752073
if T.self == Date.self {
2076-
decoded = (try self.unbox(value, as: Date.self) as! T)
2074+
guard let date = try self.unbox(value, as: Date.self) else { return nil }
2075+
decoded = date as! T
20772076
} else if T.self == Data.self {
2078-
decoded = (try self.unbox(value, as: Data.self) as! T)
2077+
guard let data = try self.unbox(value, as: Data.self) else { return nil }
2078+
decoded = data as! T
20792079
} else if T.self == URL.self {
20802080
guard let urlString = try self.unbox(value, as: String.self) else {
20812081
return nil
@@ -2088,7 +2088,8 @@ extension _JSONDecoder {
20882088

20892089
decoded = (url as! T)
20902090
} else if T.self == Decimal.self {
2091-
decoded = (try self.unbox(value, as: Decimal.self) as! T)
2091+
guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil }
2092+
decoded = decimal as! T
20922093
} else {
20932094
self.storage.push(container: value)
20942095
decoded = try T(from: self)

stdlib/public/SDK/Foundation/PlistEncoder.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,8 +1052,8 @@ fileprivate struct _PlistKeyedDecodingContainer<K : CodingKey> : KeyedDecodingCo
10521052
}
10531053

10541054
private func _superDecoder(forKey key: CodingKey) throws -> Decoder {
1055-
return try self.decoder.with(pushedKey: key) {
1056-
let value = self.container[key.stringValue]
1055+
return self.decoder.with(pushedKey: key) {
1056+
let value: Any = self.container[key.stringValue] ?? NSNull()
10571057
return _PlistDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options)
10581058
}
10591059
}
@@ -1730,13 +1730,13 @@ extension _PlistDecoder {
17301730
}
17311731

17321732
fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
1733-
if let string = value as? String, string == _plistNull { return nil }
1734-
17351733
let decoded: T
17361734
if T.self == Date.self {
1737-
decoded = (try self.unbox(value, as: Date.self) as! T)
1735+
guard let date = try self.unbox(value, as: Date.self) else { return nil }
1736+
decoded = date as! T
17381737
} else if T.self == Data.self {
1739-
decoded = (try self.unbox(value, as: Data.self) as! T)
1738+
guard let data = try self.unbox(value, as: Data.self) else { return nil }
1739+
decoded = data as! T
17401740
} else {
17411741
self.storage.push(container: value)
17421742
decoded = try T(from: self)

test/stdlib/TestJSONEncoder.swift

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ class TestJSONEncoder : TestJSONEncoderSuper {
9292
_testRoundTrip(of: employee)
9393
}
9494

95+
func testEncodingTopLevelNullableType() {
96+
// EnhancedBool is a type which encodes either as a Bool or as nil.
97+
_testEncodeFailure(of: EnhancedBool.true)
98+
_testEncodeFailure(of: EnhancedBool.false)
99+
_testEncodeFailure(of: EnhancedBool.fileNotFound)
100+
101+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.true), expectedJSON: "{\"value\":true}".data(using: .utf8)!)
102+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.false), expectedJSON: "{\"value\":false}".data(using: .utf8)!)
103+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.fileNotFound), expectedJSON: "{\"value\":null}".data(using: .utf8)!)
104+
}
105+
95106
// MARK: - Output Formatting Tests
96107
func testEncodingOutputFormattingDefault() {
97108
let expectedJSON = "{\"name\":\"Johnny Appleseed\",\"email\":\"[email protected]\"}".data(using: .utf8)!
@@ -650,61 +661,30 @@ fileprivate struct Company : Codable, Equatable {
650661
}
651662
}
652663

653-
// MARK: - Helper Types
654-
655-
/// Wraps a type T so that it can be encoded at the top level of a payload.
656-
fileprivate struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
657-
let value: T
658-
659-
init(_ value: T) {
660-
self.value = value
661-
}
662-
663-
static func ==(_ lhs: TopLevelWrapper<T>, _ rhs: TopLevelWrapper<T>) -> Bool {
664-
return lhs.value == rhs.value
665-
}
666-
}
667-
668-
fileprivate struct FloatNaNPlaceholder : Codable, Equatable {
669-
init() {}
670-
671-
func encode(to encoder: Encoder) throws {
672-
var container = encoder.singleValueContainer()
673-
try container.encode(Float.nan)
674-
}
664+
/// An enum type which decodes from Bool?.
665+
fileprivate enum EnhancedBool : Codable {
666+
case `true`
667+
case `false`
668+
case fileNotFound
675669

676670
init(from decoder: Decoder) throws {
677671
let container = try decoder.singleValueContainer()
678-
let float = try container.decode(Float.self)
679-
if !float.isNaN {
680-
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN."))
672+
if container.decodeNil() {
673+
self = .fileNotFound
674+
} else {
675+
let value = try container.decode(Bool.self)
676+
self = value ? .true : .false
681677
}
682678
}
683679

684-
static func ==(_ lhs: FloatNaNPlaceholder, _ rhs: FloatNaNPlaceholder) -> Bool {
685-
return true
686-
}
687-
}
688-
689-
fileprivate struct DoubleNaNPlaceholder : Codable, Equatable {
690-
init() {}
691-
692680
func encode(to encoder: Encoder) throws {
693681
var container = encoder.singleValueContainer()
694-
try container.encode(Double.nan)
695-
}
696-
697-
init(from decoder: Decoder) throws {
698-
let container = try decoder.singleValueContainer()
699-
let double = try container.decode(Double.self)
700-
if !double.isNaN {
701-
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN."))
682+
switch self {
683+
case .true: try container.encode(true)
684+
case .false: try container.encode(false)
685+
case .fileNotFound: try container.encodeNil()
702686
}
703687
}
704-
705-
static func ==(_ lhs: DoubleNaNPlaceholder, _ rhs: DoubleNaNPlaceholder) -> Bool {
706-
return true
707-
}
708688
}
709689

710690
/// A type which encodes as an array directly through a single value container.
@@ -853,6 +833,63 @@ struct NestedContainersTestType : Encodable {
853833
}
854834
}
855835

836+
// MARK: - Helper Types
837+
838+
/// Wraps a type T so that it can be encoded at the top level of a payload.
839+
fileprivate struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
840+
let value: T
841+
842+
init(_ value: T) {
843+
self.value = value
844+
}
845+
846+
static func ==(_ lhs: TopLevelWrapper<T>, _ rhs: TopLevelWrapper<T>) -> Bool {
847+
return lhs.value == rhs.value
848+
}
849+
}
850+
851+
fileprivate struct FloatNaNPlaceholder : Codable, Equatable {
852+
init() {}
853+
854+
func encode(to encoder: Encoder) throws {
855+
var container = encoder.singleValueContainer()
856+
try container.encode(Float.nan)
857+
}
858+
859+
init(from decoder: Decoder) throws {
860+
let container = try decoder.singleValueContainer()
861+
let float = try container.decode(Float.self)
862+
if !float.isNaN {
863+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN."))
864+
}
865+
}
866+
867+
static func ==(_ lhs: FloatNaNPlaceholder, _ rhs: FloatNaNPlaceholder) -> Bool {
868+
return true
869+
}
870+
}
871+
872+
fileprivate struct DoubleNaNPlaceholder : Codable, Equatable {
873+
init() {}
874+
875+
func encode(to encoder: Encoder) throws {
876+
var container = encoder.singleValueContainer()
877+
try container.encode(Double.nan)
878+
}
879+
880+
init(from decoder: Decoder) throws {
881+
let container = try decoder.singleValueContainer()
882+
let double = try container.decode(Double.self)
883+
if !double.isNaN {
884+
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Couldn't decode NaN."))
885+
}
886+
}
887+
888+
static func ==(_ lhs: DoubleNaNPlaceholder, _ rhs: DoubleNaNPlaceholder) -> Bool {
889+
return true
890+
}
891+
}
892+
856893
// MARK: - Run Tests
857894

858895
#if !FOUNDATION_XCTEST
@@ -870,6 +907,7 @@ JSONEncoderTests.test("testEncodingTopLevelDeepStructuredType") { TestJSONEncode
870907
JSONEncoderTests.test("testEncodingTopLevelStructuredClass") { TestJSONEncoder().testEncodingTopLevelStructuredClass() }
871908
JSONEncoderTests.test("testEncodingTopLevelDeepStructuredType") { TestJSONEncoder().testEncodingTopLevelDeepStructuredType()}
872909
JSONEncoderTests.test("testEncodingClassWhichSharesEncoderWithSuper") { TestJSONEncoder().testEncodingClassWhichSharesEncoderWithSuper() }
910+
JSONEncoderTests.test("testEncodingTopLevelNullableType") { TestJSONEncoder().testEncodingTopLevelNullableType() }
873911
JSONEncoderTests.test("testEncodingOutputFormattingDefault") { TestJSONEncoder().testEncodingOutputFormattingDefault() }
874912
JSONEncoderTests.test("testEncodingOutputFormattingPrettyPrinted") { TestJSONEncoder().testEncodingOutputFormattingPrettyPrinted() }
875913
JSONEncoderTests.test("testEncodingOutputFormattingSortedKeys") { TestJSONEncoder().testEncodingOutputFormattingSortedKeys() }

test/stdlib/TestPlistEncoder.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ class TestPropertyListEncoder : TestPropertyListEncoderSuper {
111111
_testRoundTrip(of: employee, in: .xml)
112112
}
113113

114+
func testEncodingTopLevelNullableType() {
115+
// EnhancedBool is a type which encodes either as a Bool or as nil.
116+
_testEncodeFailure(of: EnhancedBool.true, in: .binary)
117+
_testEncodeFailure(of: EnhancedBool.true, in: .xml)
118+
_testEncodeFailure(of: EnhancedBool.false, in: .binary)
119+
_testEncodeFailure(of: EnhancedBool.false, in: .xml)
120+
_testEncodeFailure(of: EnhancedBool.fileNotFound, in: .binary)
121+
_testEncodeFailure(of: EnhancedBool.fileNotFound, in: .xml)
122+
123+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.true), in: .binary)
124+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.true), in: .xml)
125+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.false), in: .binary)
126+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.false), in: .xml)
127+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.fileNotFound), in: .binary)
128+
_testRoundTrip(of: TopLevelWrapper(EnhancedBool.fileNotFound), in: .xml)
129+
}
130+
131+
114132
// MARK: - Encoder Features
115133
func testNestedContainerCodingPaths() {
116134
let encoder = JSONEncoder()
@@ -438,6 +456,32 @@ fileprivate struct Company : Codable, Equatable {
438456
}
439457
}
440458

459+
/// An enum type which decodes from Bool?.
460+
fileprivate enum EnhancedBool : Codable {
461+
case `true`
462+
case `false`
463+
case fileNotFound
464+
465+
init(from decoder: Decoder) throws {
466+
let container = try decoder.singleValueContainer()
467+
if container.decodeNil() {
468+
self = .fileNotFound
469+
} else {
470+
let value = try container.decode(Bool.self)
471+
self = value ? .true : .false
472+
}
473+
}
474+
475+
func encode(to encoder: Encoder) throws {
476+
var container = encoder.singleValueContainer()
477+
switch self {
478+
case .true: try container.encode(true)
479+
case .false: try container.encode(false)
480+
case .fileNotFound: try container.encodeNil()
481+
}
482+
}
483+
}
484+
441485
/// A type which encodes as an array directly through a single value container.
442486
struct Numbers : Codable, Equatable {
443487
let values = [4, 8, 15, 16, 23, 42]
@@ -614,6 +658,7 @@ PropertyListEncoderTests.test("testEncodingTopLevelStructuredSingleStruct") { Te
614658
PropertyListEncoderTests.test("testEncodingTopLevelStructuredSingleClass") { TestPropertyListEncoder().testEncodingTopLevelStructuredSingleClass() }
615659
PropertyListEncoderTests.test("testEncodingTopLevelDeepStructuredType") { TestPropertyListEncoder().testEncodingTopLevelDeepStructuredType() }
616660
PropertyListEncoderTests.test("testEncodingClassWhichSharesEncoderWithSuper") { TestPropertyListEncoder().testEncodingClassWhichSharesEncoderWithSuper() }
661+
PropertyListEncoderTests.test("testEncodingTopLevelNullableType") { TestPropertyListEncoder().testEncodingTopLevelNullableType() }
617662
PropertyListEncoderTests.test("testNestedContainerCodingPaths") { TestPropertyListEncoder().testNestedContainerCodingPaths() }
618663
PropertyListEncoderTests.test("testSuperEncoderCodingPaths") { TestPropertyListEncoder().testSuperEncoderCodingPaths() }
619664
runAllTests()

0 commit comments

Comments
 (0)