Skip to content

Commit 63b519b

Browse files
author
Itai Ferber
committed
Allow application of JSON strategies in all cases
One of the limitations of not having conditional conformance at the moment is that the implementation of `init(from:)` and `encode(to:)` on types which require it is that failure to cast dependent types to `Encodable` or `Decodable` is a runtime failure. There is no way to statically guarantee that the wrapped type is `Encodable` or `Decodable`. As such, in those implementations, at best we can directly call `(element as! Encodable).encode(to: encoder)`, or similar. However, this encodes the element directly into an encoder, without giving the encoder a chance to intercept the type. This is problematic for `JSONEncoder` because it cannot apply a strategy if it doesn't get to intercept the type. This gives a temporary workaround for JSON strategies because of internal Foundation knowledge.
1 parent 34a5f03 commit 63b519b

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)