Skip to content

Commit 2f130da

Browse files
committed
SR-7017: JSONDecoder will decode booleans as numbers.
- If an NSNumber was initialised from a boolean then dont allow it to be converted to a numeric value.
1 parent cd24576 commit 2f130da

File tree

3 files changed

+80
-0
lines changed

3 files changed

+80
-0
lines changed

Foundation/Codable.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ internal extension DecodingError {
3939
return "an array"
4040
} else if value is [String : Any] {
4141
return "a dictionary"
42+
} else if value is Bool {
43+
return "a boolean"
4244
} else {
4345
// This should never happen -- we somehow have a non-JSON type here.
4446
preconditionFailure("Invalid storage type \(type(of: value)).")

Foundation/JSONEncoder.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,13 @@ extension _JSONDecoder : SingleValueDecodingContainer {
17461746
// MARK: - Concrete Value Representations
17471747

17481748
extension _JSONDecoder {
1749+
1750+
private func _checkNotBoolean(_ number: NSNumber, type: Any.Type) throws {
1751+
guard number._cfTypeID != CFBooleanGetTypeID() else {
1752+
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: Bool())
1753+
}
1754+
}
1755+
17491756
/// Returns the given value unboxed from a container.
17501757
fileprivate func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
17511758
guard !(value is NSNull) else { return nil }
@@ -1769,6 +1776,7 @@ extension _JSONDecoder {
17691776
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
17701777
}
17711778

1779+
try _checkNotBoolean(number, type: type)
17721780
let int = number.intValue
17731781
guard NSNumber(value: int) == number else {
17741782
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1784,6 +1792,7 @@ extension _JSONDecoder {
17841792
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
17851793
}
17861794

1795+
try _checkNotBoolean(number, type: type)
17871796
let int8 = number.int8Value
17881797
guard NSNumber(value: int8) == number else {
17891798
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1799,6 +1808,7 @@ extension _JSONDecoder {
17991808
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18001809
}
18011810

1811+
try _checkNotBoolean(number, type: type)
18021812
let int16 = number.int16Value
18031813
guard NSNumber(value: int16) == number else {
18041814
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1814,6 +1824,7 @@ extension _JSONDecoder {
18141824
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18151825
}
18161826

1827+
try _checkNotBoolean(number, type: type)
18171828
let int32 = number.int32Value
18181829
guard NSNumber(value: int32) == number else {
18191830
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1829,6 +1840,7 @@ extension _JSONDecoder {
18291840
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18301841
}
18311842

1843+
try _checkNotBoolean(number, type: type)
18321844
let int64 = number.int64Value
18331845
guard NSNumber(value: int64) == number else {
18341846
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1844,6 +1856,7 @@ extension _JSONDecoder {
18441856
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18451857
}
18461858

1859+
try _checkNotBoolean(number, type: type)
18471860
let uint = number.uintValue
18481861
guard NSNumber(value: uint) == number else {
18491862
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1859,6 +1872,7 @@ extension _JSONDecoder {
18591872
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18601873
}
18611874

1875+
try _checkNotBoolean(number, type: type)
18621876
let uint8 = number.uint8Value
18631877
guard NSNumber(value: uint8) == number else {
18641878
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1874,6 +1888,7 @@ extension _JSONDecoder {
18741888
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18751889
}
18761890

1891+
try _checkNotBoolean(number, type: type)
18771892
let uint16 = number.uint16Value
18781893
guard NSNumber(value: uint16) == number else {
18791894
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1889,6 +1904,7 @@ extension _JSONDecoder {
18891904
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
18901905
}
18911906

1907+
try _checkNotBoolean(number, type: type)
18921908
let uint32 = number.uint32Value
18931909
guard NSNumber(value: uint32) == number else {
18941910
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1904,6 +1920,7 @@ extension _JSONDecoder {
19041920
throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value)
19051921
}
19061922

1923+
try _checkNotBoolean(number, type: type)
19071924
let uint64 = number.uint64Value
19081925
guard NSNumber(value: uint64) == number else {
19091926
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Parsed JSON number <\(number)> does not fit in \(type)."))
@@ -1916,6 +1933,7 @@ extension _JSONDecoder {
19161933
guard !(value is NSNull) else { return nil }
19171934

19181935
if let number = value as? NSNumber {
1936+
try _checkNotBoolean(number, type: type)
19191937
// We are willing to return a Float by losing precision:
19201938
// * If the original value was integral,
19211939
// * and the integral value was > Float.greatestFiniteMagnitude, we will fail
@@ -1962,6 +1980,7 @@ extension _JSONDecoder {
19621980
guard !(value is NSNull) else { return nil }
19631981

19641982
if let number = value as? NSNumber {
1983+
try _checkNotBoolean(number, type: type)
19651984
// We are always willing to return the number as a Double:
19661985
// * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double
19671986
// * If it was a Float or Double, you will get back the precise value

TestFoundation/TestJSONEncoder.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,65 @@ class TestJSONEncoder : XCTestCase {
334334
_ = try JSONDecoder().decode([Bool].self, from: "[1]".data(using: .utf8)!)
335335
XCTFail("Coercing non-boolean numbers into Bools was expected to fail")
336336
} catch { }
337+
338+
339+
// Check that a Bool false or true isnt converted to 0 or 1
340+
struct Foo: Decodable {
341+
var intValue: Int?
342+
var int8Value: Int8?
343+
var int16Value: Int16?
344+
var int32Value: Int32?
345+
var int64Value: Int64?
346+
var uintValue: UInt?
347+
var uint8Value: UInt8?
348+
var uint16Value: UInt16?
349+
var uint32Value: UInt32?
350+
var uint64Value: UInt64?
351+
var floatValue: Float?
352+
var doubleValue: Double?
353+
var decimalValue: Decimal?
354+
let boolValue: Bool
355+
}
356+
357+
func testValue(_ valueName: String) {
358+
do {
359+
let jsonData = "{ \"\(valueName)\": false }".data(using: .utf8)!
360+
_ = try JSONDecoder().decode(Foo.self, from: jsonData)
361+
XCTFail("Decoded 'false' as non Bool for \(valueName)")
362+
} catch {}
363+
do {
364+
let jsonData = "{ \"\(valueName)\": true }".data(using: .utf8)!
365+
_ = try JSONDecoder().decode(Foo.self, from: jsonData)
366+
XCTFail("Decoded 'true' as non Bool for \(valueName)")
367+
} catch {}
368+
}
369+
370+
testValue("intValue")
371+
testValue("int8Value")
372+
testValue("int16Value")
373+
testValue("int32Value")
374+
testValue("int64Value")
375+
testValue("uintValue")
376+
testValue("uint8Value")
377+
testValue("uint16Value")
378+
testValue("uint32Value")
379+
testValue("uint64Value")
380+
testValue("floatValue")
381+
testValue("doubleValue")
382+
testValue("decimalValue")
383+
let falseJsonData = "{ \"boolValue\": false }".data(using: .utf8)!
384+
if let falseFoo = try? JSONDecoder().decode(Foo.self, from: falseJsonData) {
385+
XCTAssertFalse(falseFoo.boolValue)
386+
} else {
387+
XCTFail("Could not decode 'false' as a Bool")
388+
}
389+
390+
let trueJsonData = "{ \"boolValue\": true }".data(using: .utf8)!
391+
if let trueFoo = try? JSONDecoder().decode(Foo.self, from: trueJsonData) {
392+
XCTAssertTrue(trueFoo.boolValue)
393+
} else {
394+
XCTFail("Could not decode 'true' as a Bool")
395+
}
337396
}
338397

339398
func test_codingOfInt8() {

0 commit comments

Comments
 (0)