soumyamahunt
(Soumya Ranjan Mahunt)
November 11, 2023, 1:06pm
1
I am trying to decode a Codable
model with following implementation:
struct SomeCodable {
let value1: String?
}
extension SomeCodable: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1) {
let key1_container = try key1_containerDecoder.container(keyedBy: CodingKeys.self)
self.value1 = try key1_container.decodeIfPresent(String.self, forKey: CodingKeys.value1)
} else {
self.value1 = nil
}
}
}
extension SomeCodable: Encodable {
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var key1_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.key1)
try key1_container.encodeIfPresent(self.value1, forKey: CodingKeys.value1)
}
}
extension SomeCodable {
enum CodingKeys: String, CodingKey {
case value1 = "key2"
case key1 = "key1"
}
}
If I try to decode this model from empty JSON object({}
) I get error at this line:
let key1_container = try key1_containerDecoder.container(keyedBy: CodingKeys.self)
valueNotFound(Swift.KeyedDecodingContainer<MetaCodableTests.SomeCodable.CodingKeys>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "key1", intValue: nil)], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil))"
But as superDecoder(forKey:)
documented, shouldn't the following condition have failed resulting in value1
being assigned nil
?:
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1)
Am I missing something here or is this a bug?
itaiferber
(Itai Ferber)
November 11, 2023, 3:06pm
2
soumyamahunt:
shouldn't the following condition have failed resulting in value1
being assigned nil
?:
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1)
Am I missing something here or is this a bug?
Yes, as documented, this should throw — right now, the implementation returns a decoder without checking for the presence of the key first:
private func _superDecoder(forKey key: __owned CodingKey) throws -> Decoder {
self.decoder.codingPath.append(key)
defer { self.decoder.codingPath.removeLast() }
let value: Any = self.container[key.stringValue] ?? NSNull() // <- should throw instead of substituting `NSNull`
return __JSONDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options)
}
I'd consider this a bug worth a report.
FWIW, the upcoming swift-foundation implementation appears to behave the same right now, so doubly-worth a report there while the implementation is still being built out:
func superDecoder(forKey key: K) throws -> Decoder {
return decoderForKeyNoThrow(key)
}
private func decoderForKeyNoThrow(_ key: some CodingKey) -> JSONDecoderImpl {
let value: JSONMap.Value
do {
value = try getValue(forKey: key)
} catch {
// if there no value for this key then return a null value
value = .null
}
let impl = JSONDecoderImpl(userInfo: self.impl.userInfo, from: self.impl.jsonMap, codingPathNode: self.codingPathNode.pushing(key), options: self.impl.options)
impl.push(value: value)
return impl
}
(cc @jmschonfeld )
3 Likes
soumyamahunt
(Soumya Ranjan Mahunt)
November 13, 2023, 8:12am
3
Thanks @itaiferber , I have reported this bug in both the repos:
opened 08:10AM - 13 Nov 23 UTC
When trying to decode a `Codable` model with following implementation:
```swi… ft
struct SomeCodable {
let value1: String?
}
extension SomeCodable: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1) {
let key1_container = try key1_containerDecoder.container(keyedBy: CodingKeys.self)
self.value1 = try key1_container.decodeIfPresent(String.self, forKey: CodingKeys.value1)
} else {
self.value1 = nil
}
}
}
extension SomeCodable: Encodable {
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var key1_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.key1)
try key1_container.encodeIfPresent(self.value1, forKey: CodingKeys.value1)
}
}
extension SomeCodable {
enum CodingKeys: String, CodingKey {
case value1 = "key2"
case key1 = "key1"
}
}
```
and empty JSON object(`{}`) error is thrown at
```swift
let key1_container = try key1_containerDecoder.container(keyedBy: CodingKeys.self)
```
> valueNotFound(Swift.KeyedDecodingContainer<MetaCodableTests.SomeCodable.CodingKeys>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "key1", intValue: nil)], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil))"
instead of
```swift
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1)
```
which is diferrent than [documented](https://developer.apple.com/documentation/swift/keyeddecodingcontainer/superdecoder(forkey:)).
As per discussion in [forum](https://forums.swift.org/t/superdecoder-forkey-doesnt-work-as-documented/68407), both older and new `Foundation` implementation seems to have this bug.
opened 08:11AM - 13 Nov 23 UTC
When trying to decode a `Codable` model with following implementation:
```swi… ft
struct SomeCodable {
let value1: String?
}
extension SomeCodable: Decodable {
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1) {
let key1_container = try key1_containerDecoder.container(keyedBy: CodingKeys.self)
self.value1 = try key1_container.decodeIfPresent(String.self, forKey: CodingKeys.value1)
} else {
self.value1 = nil
}
}
}
extension SomeCodable: Encodable {
func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var key1_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.key1)
try key1_container.encodeIfPresent(self.value1, forKey: CodingKeys.value1)
}
}
extension SomeCodable {
enum CodingKeys: String, CodingKey {
case value1 = "key2"
case key1 = "key1"
}
}
```
and empty JSON object(`{}`) error is thrown at
```swift
let key1_container = try key1_containerDecoder.container(keyedBy: CodingKeys.self)
```
> valueNotFound(Swift.KeyedDecodingContainer<MetaCodableTests.SomeCodable.CodingKeys>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "key1", intValue: nil)], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil))"
instead of
```swift
if let key1_containerDecoder = try? container.superDecoder(forKey: CodingKeys.key1)
```
which is diferrent than [documented](https://developer.apple.com/documentation/swift/keyeddecodingcontainer/superdecoder(forkey:)).
As per discussion in [forum](https://forums.swift.org/t/superdecoder-forkey-doesnt-work-as-documented/68407), both older and new `Foundation` implementation seems to have this bug.
1 Like
jmschonfeld
(Jeremy Schonfeld)
November 13, 2023, 4:59pm
4
Thanks for the heads up @itaiferber ! I agree it does look like the behavior doesn't match our documentation so we should ensure one or the other is updated so they match. I'll make sure I bring this up to Charles and Kevin who have been working in this area recently.
1 Like
itaiferber
(Itai Ferber)
November 13, 2023, 5:13pm
5
Thanks, @jmschonfeld ! FWIW, I'd lean toward updating the implementation to match the documentation (and consider my original implementation a bug); I can't imagine any significant backwards-compatibility concerns (superDecoder(...)
is already pretty niche), so it might be better to match the docs. (Though of course, that's up to the team to explore.)
2 Likes
jmschonfeld
(Jeremy Schonfeld)
November 13, 2023, 5:14pm
6
Yeah that makes sense to me as well. cc @glessard since you mentioned to me you might take this one on