Is there any way to fetch the original decoder for a KeyedDecodingContainer?

To simplify the way I use Codable, I created https://github.com/miku1958/HappyCodable that uses property wrapper to support multiple key mapping, ignore specific coding keys, synthesis for non-RawRepresentable enums, and etc, by using SourceKittenFramework to replace the work of the compiler. It's not perfect, but it works great most of the time.

Someone recently did the same thing by using Mirror to fetch the decoder and cache the first initialization of all the data for the property wrappers when decoding. In this way, I can only use the property wrapper to achieve what I did, except synthesize non-RawRepresentable enums, but that is not safe. Once you are not using JSONDecoder, it will probably crash.

It looks great, so I accomplished most of it in a safer way, except I can't fetch the decoder without using Mirror.

I searched through the entire API of Codable but found nothing.

I try to cheat the complier to let the decoder call my function to create a container so I can cache the decoder. And it works, but crashes when container decoding:

extension Decoder {
// I need to use an existing type to make sure the compiler won't call the original function.
	public func container(keyedBy type: Any.Type) throws -> KeyedDecodingContainer<StringCodingKey> { 
		typealias OriContainer<Key: CodingKey> = (Key.Type) throws -> KeyedDecodingContainer<Key>
		let ori: OriContainer<StringCodingKey> = self.container(keyedBy:)
		let container =  try ori(StringCodingKey.self)
		return container
	}
}

I am so desperate, is there any way I can fetch the original decoder without using Mirror?

You generally can't retrieve the decoder from a container. It's not guaranteed that there's anything there. It's possible for a container to only reference some Context object that is shared with the decoder but not the decoder itself.

Is there any way to convert KeyedDecodingContainer to another Key type? I haven't found an API for doing this. :frowning_face:

There's no safe way to simply cast between two unrelated types that I'm aware of, and Container<A> and Container<B> are unrelated alright.


It's also worth mentioning that you can define a custom decode(_:forKey:) function which will be picked up by the synthesizer. It should give you enough leeway to ignore/redirect the key. This technique can be gleaned from ResilientDecoding. Note that the wrapper still needs to conform to Decoder. As an example, the following code uses our custom decode function:

import Foundation 

@propertyWrapper
struct TestWrap<T: ExpressibleByIntegerLiteral>: Decodable {
    var wrappedValue: T
    
    init(from decoder: Decoder) throws { fatalError() }
    init() { wrappedValue = 3 }
}
extension KeyedDecodingContainer {
    func decode<T>(_: TestWrap<T>.Type, forKey: Key) -> TestWrap<T> {
        .init()
    }
}
struct Test: Decodable {
    @TestWrap var a: Int
}

let raw = #"{}"#, decoder = JSONDecoder()
let result = try decoder.decode(Test.self, from: .init(raw.utf8))
print(result.a) // 3

Try not to use this trick too much, though. It can be tricky to reason with.

I know how to use custom decode function and I try this way to create a container from decoder to cache the decoder but it doesn't work well...

I just checked the Swift source code and found that the only reason KeyedDecodingContainer has an associated type is to unsafeBitCast the key type.... but they are only using the CodingKey to subscribe to an NSMutableDictionary. I am still don't understand why they don't just pass any CodingKey type to the KeyedDecodingContainer when decoding. :sweat:

What do you plan to do with the cached decoder?

  • If you want to pass in information, you could use userInfo dictionary though you need to perform some set up before the decoding starts.
  • If you want to decode keys other than ones presented in Key, that shouldn't be possible. Some containers discard irrelevant fields.

I'm not sure I understand your use-case perfectly, so correct me if I'm wrong: are you looking to hold on to the Decoder instance so that on access through the property wrapper you can decode in a specific way?

I will say that it is entirely unsafe to hold on to a Decoder (or coding container) for any amount of time beyond the end of an init(from:) call, no matter how you get at the Decoder. Decoders can be classes with mutable state that changes over time so that accessing their contents after-the-fact can very easily crash. Even in the case of JSONDecoder (or really, _JSONDecoder, its inner type), you cannot hold on to a decoder or container because it maintains an internal stack of values that changes as soon as init(from:) returns.

Is this what you had in mind or am I totally off-base?

I am trying the second one, where I want to decode some property with a different key like decode
"{ "id": 123 }"
to

struct User {
    let jobID: Int
}

A workable solution is passing the "id" to the JSONDecoder, but this is not automatic, I did can package the JSONDecoder to make it automatic to get the altered decode key from the property wrapper, but it won't work if I use sqlite library based on Codable like WCDB.swift to store it.

In this case, I am only trying to hold the decoder to create a KeyedDecodingContainer with a different Key type. And it only happens in the init(from:), because the property wrapper I create is countable, and when I make sure I enumerate all property wrappers, I can release the decoder before init(from:) finishes.

It wouldn't work with my decoder either (well, one version of it). The container removes entries with no associated keys since I need to transform the internal representation to Keys anyway. And I'm pretty sure that at that point in the program (after decode(_:forKey:)), there isn't anyway to retrieve the discarded fields. So it's definitely not applicable to all Decoders.

Yes, to resolve that, it has to make sure call the decode function with the original CodingKeys first.
Then decode from alter keys if fail, which is why some code HappyCodable generated looks so cumbersome

Hmm, which wrapper is this? I can take a look. I could be wrong but I'm most certain that whatever you do won't work with my decoder. All area a property wrapper can influence are after the data is discarded, so there shouldn't be a way to retrieve it.

You mean which property wrapper on HappyCodable? I use SourceKitten to generate the init(from:) directly rather than extend KeyedDecodingContainer ... So the effect of the property wrappers are only for locating what I need to do.

I'm trying to say that what you're trying to achieve is impossible for arbitrary decoder. My KeyedBinaryDecodingContainer doesn't contain decoder, and the field you're looking for is discarded during the init*. Changing Key won't help since it'll just treat the data as missing that field.

It may work with decoders in a whitelist (JSONDecoder, PListDecoder, etc.) using Mirror trick that you mentioned.


* The implementation doesn't meet my requirement, so I did not actually discard any data during init. Regardless, it's a valid implementation.

But the CodingKeys is not enumerable required, so the decoder doesn't know which keys should be discarded until a special decoder creates a container with specific CodingKeys to cache necessary CodingKeys (like WCDB.swift) from the beginning. If you can intervene with init(from:), you can change the CodingKeys you used.

That is still long before the earliest time you can intervene. Unless you write your own init(from:) of course, but then you want to use the synthesized one.

I want to remove the part about writing my own init(from:) so it can work in the private model. I have to find a way to change the Key type of KeyedDecodingContainer to a AnyCodingKeys when KeyedDecodingContainer.decode(), I checked the source code and it looks like it's not gonna happen. I try to fetch the decoder and fail because the KeyedDecodingContainer is a package of another package of the real DecodingContainer. Then I try to cheat the complier into calling my container create function but it crashes after KeyedDecodingContainer.decode(). I have tried a custom JSONDecoder, it works mostly but is not suitable for a third-party library based on Codable. Why is it so difficult.

Well, I did say it's impossible a few times. Synthesis is not meant to be a general infinitely hackable, infinitely configurable process. You are trying to do something it is not designed for. That's precisely why you have a write-your-own-init(from:) as an escape hatch.

There are some requests for improvements over the year of course, but I still doubt it'll get to the level that you can achieve what you want.

1 Like

indeed

Terms of Service

Privacy Policy

Cookie Policy