Skip to content

Commit 26e35dd

Browse files
authored
Merge pull request #10818 from itaiferber/4.0-jsonencoder-conditional-conformance-workarounds
[4.0] JSONEncoder conditional conformance workarounds
2 parents 732f90d + 63b519b commit 26e35dd

File tree

7 files changed

+274
-26
lines changed

7 files changed

+274
-26
lines changed

stdlib/public/SDK/Foundation/Data.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1785,11 +1785,27 @@ extension NSData : _HasCustomAnyHashableRepresentation {
17851785

17861786
extension Data : Codable {
17871787
public init(from decoder: Decoder) throws {
1788+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
1789+
do {
1790+
let singleValueContainer = try decoder.singleValueContainer()
1791+
if let decoder = singleValueContainer as? _JSONDecoder {
1792+
switch decoder.options.dataDecodingStrategy {
1793+
case .deferredToData:
1794+
break /* fall back to default implementation below; this would recurse */
1795+
1796+
default:
1797+
// _JSONDecoder has a hook for Datas; this won't recurse since we're not going to defer back to Data in _JSONDecoder.
1798+
self = try singleValueContainer.decode(Data.self)
1799+
return
1800+
}
1801+
}
1802+
} catch { /* fall back to default implementation below */ }
1803+
17881804
var container = try decoder.unkeyedContainer()
17891805

17901806
// It's more efficient to pre-allocate the buffer if we can.
17911807
if let count = container.count {
1792-
self.init(count: count)
1808+
self = Data(count: count)
17931809

17941810
// Loop only until count, not while !container.isAtEnd, in case count is underestimated (this is misbehavior) and we haven't allocated enough space.
17951811
// We don't want to write past the end of what we allocated.
@@ -1798,7 +1814,7 @@ extension Data : Codable {
17981814
self[i] = byte
17991815
}
18001816
} else {
1801-
self.init()
1817+
self = Data()
18021818
}
18031819

18041820
while !container.isAtEnd {
@@ -1808,6 +1824,21 @@ extension Data : Codable {
18081824
}
18091825

18101826
public func encode(to encoder: Encoder) throws {
1827+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
1828+
// We are allowed to request this container as long as we don't encode anything through it when we need the unkeyed container below.
1829+
var singleValueContainer = encoder.singleValueContainer()
1830+
if let encoder = singleValueContainer as? _JSONEncoder {
1831+
switch encoder.options.dataEncodingStrategy {
1832+
case .deferredToData:
1833+
break /* fall back to default implementation below; this would recurse */
1834+
1835+
default:
1836+
// _JSONEncoder has a hook for Datas; this won't recurse since we're not going to defer back to Data in _JSONEncoder.
1837+
try singleValueContainer.encode(self)
1838+
return
1839+
}
1840+
}
1841+
18111842
var container = encoder.unkeyedContainer()
18121843

18131844
// Since enumerateBytes does not rethrow, we need to catch the error, stow it away, and rethrow if we stopped.

stdlib/public/SDK/Foundation/Date.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,13 +287,40 @@ extension Date : CustomPlaygroundQuickLookable {
287287

288288
extension Date : Codable {
289289
public init(from decoder: Decoder) throws {
290+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
290291
let container = try decoder.singleValueContainer()
292+
if let decoder = container as? _JSONDecoder {
293+
switch decoder.options.dateDecodingStrategy {
294+
case .deferredToDate:
295+
break /* fall back to default implementation below; this would recurse */
296+
297+
default:
298+
// _JSONDecoder has a hook for Dates; this won't recurse since we're not going to defer back to Date in _JSONDecoder.
299+
self = try container.decode(Date.self)
300+
return
301+
}
302+
}
303+
291304
let timestamp = try container.decode(Double.self)
292-
self.init(timeIntervalSinceReferenceDate: timestamp)
305+
self = Date(timeIntervalSinceReferenceDate: timestamp)
293306
}
294307

295308
public func encode(to encoder: Encoder) throws {
309+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
310+
// We are allowed to request this container as long as we don't encode anything through it when we need the keyed container below.
296311
var container = encoder.singleValueContainer()
312+
if let encoder = container as? _JSONEncoder {
313+
switch encoder.options.dateEncodingStrategy {
314+
case .deferredToDate:
315+
break /* fall back to default implementation below; this would recurse */
316+
317+
default:
318+
// _JSONEncoder has a hook for Dates; this won't recurse since we're not going to defer back to Date in _JSONEncoder.
319+
try container.encode(self)
320+
return
321+
}
322+
}
323+
297324
try container.encode(self.timeIntervalSinceReferenceDate)
298325
}
299326
}

stdlib/public/SDK/Foundation/Decimal.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,17 @@ extension Decimal : Codable {
470470
}
471471

472472
public init(from decoder: Decoder) throws {
473+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
474+
do {
475+
// We are allowed to request this container as long as we don't decode anything through it when we need the keyed container below.
476+
let singleValueContainer = try decoder.singleValueContainer()
477+
if singleValueContainer is _JSONDecoder {
478+
// _JSONDecoder has a hook for Decimals; this won't recurse since we're not going to defer to Decimal in _JSONDecoder.
479+
self = try singleValueContainer.decode(Decimal.self)
480+
return
481+
}
482+
} catch { /* Fall back to default implementation below. */ }
483+
473484
let container = try decoder.container(keyedBy: CodingKeys.self)
474485
let exponent = try container.decode(CInt.self, forKey: .exponent)
475486
let length = try container.decode(CUnsignedInt.self, forKey: .length)
@@ -488,15 +499,24 @@ extension Decimal : Codable {
488499
mantissa.6 = try mantissaContainer.decode(CUnsignedShort.self)
489500
mantissa.7 = try mantissaContainer.decode(CUnsignedShort.self)
490501

491-
self.init(_exponent: exponent,
492-
_length: length,
493-
_isNegative: CUnsignedInt(isNegative ? 1 : 0),
494-
_isCompact: CUnsignedInt(isCompact ? 1 : 0),
495-
_reserved: 0,
496-
_mantissa: mantissa)
502+
self = Decimal(_exponent: exponent,
503+
_length: length,
504+
_isNegative: CUnsignedInt(isNegative ? 1 : 0),
505+
_isCompact: CUnsignedInt(isCompact ? 1 : 0),
506+
_reserved: 0,
507+
_mantissa: mantissa)
497508
}
498509

499510
public func encode(to encoder: Encoder) throws {
511+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
512+
// We are allowed to request this container as long as we don't encode anything through it when we need the keyed container below.
513+
var singleValueContainer = encoder.singleValueContainer()
514+
if singleValueContainer is _JSONEncoder {
515+
// _JSONEncoder has a hook for Decimals; this won't recurse since we're not going to defer to Decimal in _JSONEncoder.
516+
try singleValueContainer.encode(self)
517+
return
518+
}
519+
500520
var container = encoder.container(keyedBy: CodingKeys.self)
501521
try container.encode(_exponent, forKey: .exponent)
502522
try container.encode(_length, forKey: .length)

stdlib/public/SDK/Foundation/JSONEncoder.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ open class JSONEncoder {
9999
open var userInfo: [CodingUserInfoKey : Any] = [:]
100100

101101
/// Options set on the top-level encoder to pass down the encoding hierarchy.
102-
fileprivate struct _Options {
102+
internal struct _Options {
103103
let dateEncodingStrategy: DateEncodingStrategy
104104
let dataEncodingStrategy: DataEncodingStrategy
105105
let nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy
@@ -155,14 +155,14 @@ open class JSONEncoder {
155155

156156
// MARK: - _JSONEncoder
157157

158-
fileprivate class _JSONEncoder : Encoder {
158+
internal class _JSONEncoder : Encoder {
159159
// MARK: Properties
160160

161161
/// The encoder's storage.
162162
fileprivate var storage: _JSONEncodingStorage
163163

164164
/// Options set on the top-level encoder.
165-
fileprivate let options: JSONEncoder._Options
165+
internal let options: JSONEncoder._Options
166166

167167
/// The path to the current point in encoding.
168168
public var codingPath: [CodingKey]
@@ -864,7 +864,7 @@ open class JSONDecoder {
864864
open var userInfo: [CodingUserInfoKey : Any] = [:]
865865

866866
/// Options set on the top-level encoder to pass down the decoding hierarchy.
867-
fileprivate struct _Options {
867+
internal struct _Options {
868868
let dateDecodingStrategy: DateDecodingStrategy
869869
let dataDecodingStrategy: DataDecodingStrategy
870870
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
@@ -908,14 +908,14 @@ open class JSONDecoder {
908908

909909
// MARK: - _JSONDecoder
910910

911-
fileprivate class _JSONDecoder : Decoder {
911+
internal class _JSONDecoder : Decoder {
912912
// MARK: Properties
913913

914914
/// The decoder's storage.
915915
fileprivate var storage: _JSONDecodingStorage
916916

917917
/// Options set on the top-level decoder.
918-
fileprivate let options: JSONDecoder._Options
918+
internal let options: JSONDecoder._Options
919919

920920
/// The path to the current point in encoding.
921921
private(set) public var codingPath: [CodingKey]

stdlib/public/SDK/Foundation/URL.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,17 @@ extension URL : Codable {
12141214
}
12151215

12161216
public init(from decoder: Decoder) throws {
1217+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
1218+
do {
1219+
// We are allowed to request this container as long as we don't decode anything through it when we need the keyed container below.
1220+
let singleValueContainer = try decoder.singleValueContainer()
1221+
if singleValueContainer is _JSONDecoder {
1222+
// _JSONDecoder has a hook for URLs; this won't recurse since we're not going to defer back to URL in _JSONDecoder.
1223+
self = try singleValueContainer.decode(URL.self)
1224+
return
1225+
}
1226+
} catch { /* Fall back to default implementation below. */ }
1227+
12171228
let container = try decoder.container(keyedBy: CodingKeys.self)
12181229
let relative = try container.decode(String.self, forKey: .relative)
12191230
let base = try container.decodeIfPresent(URL.self, forKey: .base)
@@ -1227,6 +1238,15 @@ extension URL : Codable {
12271238
}
12281239

12291240
public func encode(to encoder: Encoder) throws {
1241+
// FIXME: This is a hook for bypassing a conditional conformance implementation to apply a strategy (see SR-5206). Remove this once conditional conformance is available.
1242+
// We are allowed to request this container as long as we don't encode anything through it when we need the keyed container below.
1243+
var singleValueContainer = encoder.singleValueContainer()
1244+
if singleValueContainer is _JSONEncoder {
1245+
// _JSONEncoder has a hook for URLs; this won't recurse since we're not going to defer back to URL in _JSONEncoder.
1246+
try singleValueContainer.encode(self)
1247+
return
1248+
}
1249+
12301250
var container = encoder.container(keyedBy: CodingKeys.self)
12311251
try container.encode(self.relativeString, forKey: .relative)
12321252
if let base = self.baseURL {

test/stdlib/CodableTests.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,17 @@ class TestCodable : TestCodableSuper {
389389
Decimal.greatestFiniteMagnitude,
390390
Decimal.leastNormalMagnitude,
391391
Decimal.leastNonzeroMagnitude,
392-
Decimal.pi,
393-
Decimal()
392+
Decimal(),
393+
394+
// Decimal.pi does not round-trip at the moment.
395+
// See rdar://problem/33165355
396+
// Decimal.pi,
394397
]
395398

396399
func test_Decimal_JSON() {
397400
for decimal in decimalValues {
398-
expectRoundTripEqualityThroughJSON(for: decimal)
401+
// Decimal encodes as a number in JSON and cannot be encoded at the top level.
402+
expectRoundTripEqualityThroughJSON(for: TopLevelWrapper(decimal))
399403
}
400404
}
401405

@@ -568,7 +572,16 @@ class TestCodable : TestCodableSuper {
568572

569573
func test_URL_JSON() {
570574
for url in urlValues {
571-
expectRoundTripEqualityThroughJSON(for: url)
575+
// URLs encode as single strings in JSON. They lose their baseURL this way.
576+
// For relative URLs, we don't expect them to be equal to the original.
577+
if url.baseURL == nil {
578+
// This is an absolute URL; we can expect equality.
579+
expectRoundTripEqualityThroughJSON(for: TopLevelWrapper(url))
580+
} else {
581+
// This is a relative URL. Make it absolute first.
582+
let absoluteURL = URL(string: url.absoluteString)!
583+
expectRoundTripEqualityThroughJSON(for: TopLevelWrapper(absoluteURL))
584+
}
572585
}
573586
}
574587

@@ -601,6 +614,22 @@ class TestCodable : TestCodableSuper {
601614
}
602615
}
603616

617+
// MARK: - Helper Types
618+
619+
struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
620+
let value: T
621+
622+
init(_ value: T) {
623+
self.value = value
624+
}
625+
626+
static func ==(_ lhs: TopLevelWrapper<T>, _ rhs: TopLevelWrapper<T>) -> Bool {
627+
return lhs.value == rhs.value
628+
}
629+
}
630+
631+
// MARK: - Tests
632+
604633
#if !FOUNDATION_XCTEST
605634
var CodableTests = TestSuite("TestCodable")
606635

0 commit comments

Comments
 (0)