Skip to content

[4.0] JSONEncoder conditional conformance workarounds #10818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions stdlib/public/SDK/Foundation/Data.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1785,11 +1785,27 @@ extension NSData : _HasCustomAnyHashableRepresentation {

extension Data : Codable {
public init(from decoder: Decoder) throws {
// 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.
do {
let singleValueContainer = try decoder.singleValueContainer()
if let decoder = singleValueContainer as? _JSONDecoder {
switch decoder.options.dataDecodingStrategy {
case .deferredToData:
break /* fall back to default implementation below; this would recurse */

default:
// _JSONDecoder has a hook for Datas; this won't recurse since we're not going to defer back to Data in _JSONDecoder.
self = try singleValueContainer.decode(Data.self)
return
}
}
} catch { /* fall back to default implementation below */ }

var container = try decoder.unkeyedContainer()

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

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

while !container.isAtEnd {
Expand All @@ -1808,6 +1824,21 @@ extension Data : Codable {
}

public func encode(to encoder: Encoder) throws {
// 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.
// We are allowed to request this container as long as we don't encode anything through it when we need the unkeyed container below.
var singleValueContainer = encoder.singleValueContainer()
if let encoder = singleValueContainer as? _JSONEncoder {
switch encoder.options.dataEncodingStrategy {
case .deferredToData:
break /* fall back to default implementation below; this would recurse */

default:
// _JSONEncoder has a hook for Datas; this won't recurse since we're not going to defer back to Data in _JSONEncoder.
try singleValueContainer.encode(self)
return
}
}

var container = encoder.unkeyedContainer()

// Since enumerateBytes does not rethrow, we need to catch the error, stow it away, and rethrow if we stopped.
Expand Down
29 changes: 28 additions & 1 deletion stdlib/public/SDK/Foundation/Date.swift
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,40 @@ extension Date : CustomPlaygroundQuickLookable {

extension Date : Codable {
public init(from decoder: Decoder) throws {
// 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.
let container = try decoder.singleValueContainer()
if let decoder = container as? _JSONDecoder {
switch decoder.options.dateDecodingStrategy {
case .deferredToDate:
break /* fall back to default implementation below; this would recurse */

default:
// _JSONDecoder has a hook for Dates; this won't recurse since we're not going to defer back to Date in _JSONDecoder.
self = try container.decode(Date.self)
return
}
}

let timestamp = try container.decode(Double.self)
self.init(timeIntervalSinceReferenceDate: timestamp)
self = Date(timeIntervalSinceReferenceDate: timestamp)
}

public func encode(to encoder: Encoder) throws {
// 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.
// We are allowed to request this container as long as we don't encode anything through it when we need the keyed container below.
var container = encoder.singleValueContainer()
if let encoder = container as? _JSONEncoder {
switch encoder.options.dateEncodingStrategy {
case .deferredToDate:
break /* fall back to default implementation below; this would recurse */

default:
// _JSONEncoder has a hook for Dates; this won't recurse since we're not going to defer back to Date in _JSONEncoder.
try container.encode(self)
return
}
}

try container.encode(self.timeIntervalSinceReferenceDate)
}
}
32 changes: 26 additions & 6 deletions stdlib/public/SDK/Foundation/Decimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,17 @@ extension Decimal : Codable {
}

public init(from decoder: Decoder) throws {
// 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.
do {
// We are allowed to request this container as long as we don't decode anything through it when we need the keyed container below.
let singleValueContainer = try decoder.singleValueContainer()
if singleValueContainer is _JSONDecoder {
// _JSONDecoder has a hook for Decimals; this won't recurse since we're not going to defer to Decimal in _JSONDecoder.
self = try singleValueContainer.decode(Decimal.self)
return
}
} catch { /* Fall back to default implementation below. */ }

let container = try decoder.container(keyedBy: CodingKeys.self)
let exponent = try container.decode(CInt.self, forKey: .exponent)
let length = try container.decode(CUnsignedInt.self, forKey: .length)
Expand All @@ -488,15 +499,24 @@ extension Decimal : Codable {
mantissa.6 = try mantissaContainer.decode(CUnsignedShort.self)
mantissa.7 = try mantissaContainer.decode(CUnsignedShort.self)

self.init(_exponent: exponent,
_length: length,
_isNegative: CUnsignedInt(isNegative ? 1 : 0),
_isCompact: CUnsignedInt(isCompact ? 1 : 0),
_reserved: 0,
_mantissa: mantissa)
self = Decimal(_exponent: exponent,
_length: length,
_isNegative: CUnsignedInt(isNegative ? 1 : 0),
_isCompact: CUnsignedInt(isCompact ? 1 : 0),
_reserved: 0,
_mantissa: mantissa)
}

public func encode(to encoder: Encoder) throws {
// 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.
// We are allowed to request this container as long as we don't encode anything through it when we need the keyed container below.
var singleValueContainer = encoder.singleValueContainer()
if singleValueContainer is _JSONEncoder {
// _JSONEncoder has a hook for Decimals; this won't recurse since we're not going to defer to Decimal in _JSONEncoder.
try singleValueContainer.encode(self)
return
}

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(_exponent, forKey: .exponent)
try container.encode(_length, forKey: .length)
Expand Down
12 changes: 6 additions & 6 deletions stdlib/public/SDK/Foundation/JSONEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ open class JSONEncoder {
open var userInfo: [CodingUserInfoKey : Any] = [:]

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

// MARK: - _JSONEncoder

fileprivate class _JSONEncoder : Encoder {
internal class _JSONEncoder : Encoder {
// MARK: Properties

/// The encoder's storage.
fileprivate var storage: _JSONEncodingStorage

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

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

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

// MARK: - _JSONDecoder

fileprivate class _JSONDecoder : Decoder {
internal class _JSONDecoder : Decoder {
// MARK: Properties

/// The decoder's storage.
fileprivate var storage: _JSONDecodingStorage

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

/// The path to the current point in encoding.
private(set) public var codingPath: [CodingKey]
Expand Down
20 changes: 20 additions & 0 deletions stdlib/public/SDK/Foundation/URL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,17 @@ extension URL : Codable {
}

public init(from decoder: Decoder) throws {
// 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.
do {
// We are allowed to request this container as long as we don't decode anything through it when we need the keyed container below.
let singleValueContainer = try decoder.singleValueContainer()
if singleValueContainer is _JSONDecoder {
// _JSONDecoder has a hook for URLs; this won't recurse since we're not going to defer back to URL in _JSONDecoder.
self = try singleValueContainer.decode(URL.self)
return
}
} catch { /* Fall back to default implementation below. */ }

let container = try decoder.container(keyedBy: CodingKeys.self)
let relative = try container.decode(String.self, forKey: .relative)
let base = try container.decodeIfPresent(URL.self, forKey: .base)
Expand All @@ -1227,6 +1238,15 @@ extension URL : Codable {
}

public func encode(to encoder: Encoder) throws {
// 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.
// We are allowed to request this container as long as we don't encode anything through it when we need the keyed container below.
var singleValueContainer = encoder.singleValueContainer()
if singleValueContainer is _JSONEncoder {
// _JSONEncoder has a hook for URLs; this won't recurse since we're not going to defer back to URL in _JSONEncoder.
try singleValueContainer.encode(self)
return
}

var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.relativeString, forKey: .relative)
if let base = self.baseURL {
Expand Down
37 changes: 33 additions & 4 deletions test/stdlib/CodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,13 +389,17 @@ class TestCodable : TestCodableSuper {
Decimal.greatestFiniteMagnitude,
Decimal.leastNormalMagnitude,
Decimal.leastNonzeroMagnitude,
Decimal.pi,
Decimal()
Decimal(),

// Decimal.pi does not round-trip at the moment.
// See rdar://problem/33165355
// Decimal.pi,
]

func test_Decimal_JSON() {
for decimal in decimalValues {
expectRoundTripEqualityThroughJSON(for: decimal)
// Decimal encodes as a number in JSON and cannot be encoded at the top level.
expectRoundTripEqualityThroughJSON(for: TopLevelWrapper(decimal))
}
}

Expand Down Expand Up @@ -568,7 +572,16 @@ class TestCodable : TestCodableSuper {

func test_URL_JSON() {
for url in urlValues {
expectRoundTripEqualityThroughJSON(for: url)
// URLs encode as single strings in JSON. They lose their baseURL this way.
// For relative URLs, we don't expect them to be equal to the original.
if url.baseURL == nil {
// This is an absolute URL; we can expect equality.
expectRoundTripEqualityThroughJSON(for: TopLevelWrapper(url))
} else {
// This is a relative URL. Make it absolute first.
let absoluteURL = URL(string: url.absoluteString)!
expectRoundTripEqualityThroughJSON(for: TopLevelWrapper(absoluteURL))
}
}
}

Expand Down Expand Up @@ -601,6 +614,22 @@ class TestCodable : TestCodableSuper {
}
}

// MARK: - Helper Types

struct TopLevelWrapper<T> : Codable, Equatable where T : Codable, T : Equatable {
let value: T

init(_ value: T) {
self.value = value
}

static func ==(_ lhs: TopLevelWrapper<T>, _ rhs: TopLevelWrapper<T>) -> Bool {
return lhs.value == rhs.value
}
}

// MARK: - Tests

#if !FOUNDATION_XCTEST
var CodableTests = TestSuite("TestCodable")

Expand Down
Loading