Allowing top-level fragments in JSONDecoder

Recently I had to work with a JSON API that returned string values as top-level JSON objects. I was surprised to learn that JSONDecoder doesn’t support fragments as top-level objects, while JSONSerialization does.

let data = "\"foo\"".data(using: .utf8)!
try JSONSerialization.jsonObject(with: data, options: .allowFragments) // "foo"
try JSONDecoder().decode(String.self, from: data) // nope

I’m considering proposing a solution using a new JSONDecoder.FragmentDecodingStrategy enum:

open class JSONDecoder {

    /// The strategy to use upon encountering a top-level fragment which is
    /// neither `Array` nor `Dictionary`.
    public enum FragmentDecodingStrategy {

        /// Throw upon encountering a top-level fragment. Default behavior.
        case `throw`

        /// Allow top-level fragments to be decoded into a type expecting
        /// a single value container.
        case allow

    }

    /// The strategy to use when encountering a top-level fragment.
    open var fragmentDecodingStrategy: FragmentDecodingStrategy = .`throw`

}

The internal implementation would pass .allowFragments option to JSONSerialization if fragmentDecodingStrategy is set to .allow.

Usage:

let stringData = "\"foo\"".data(using: .utf8)!
let intData = "42".data(using: .utf8)!

let decoder = JSONDecoder()

decoder.fragmentDecodingStrategy = .allow

try decoder.decode(String.self, from: stringData) // "foo"
try decoder.decode(Int.self, from: intData) // 42

decoder.fragmentDecodingStrategy = .`throw`

try decoder.decode(String.self, from: stringData) // error
try decoder.decode(Int.self, from: intData) // error

This would be an additive change.

I’d like to know your opinions about the problem, the proposed solution and whether you think that this issue is significant enough to warrant a proposal.

4 Likes

Thanks for bringing this up! We’ve had requests for this in the past on both encoding and decoding (see SR-6163), and I think it’s something that we intend to support. Like most other Foundation API changes, this sort of thing would require going through internal review, not Swift Evolution, so your best bet would be to file a Radar with the request and we can track it internally.

2 Likes

Wait, are you saying JSONDecoder is under Apple control and not part of the open source project?

The implementation is open-source, but still part of the Foundation project. We’re looking to open up the process a bit more with things like OrderedSet, but API changes that affect Foundation do still have to go through our internal review process as well, yes. Especially if the change would require changes to non-open-source Foundation code, like adding the option to encode JSON fragments (which would require new API on NSJSONSerialization on Darwin to support).

1 Like

JSONSerialization already has a setting for allowing fragments, JSONSerialization.ReadingOptions.allowFragments, as shown in the OP. So it seems like this change would just be exposing that option through JSONDecoder as well. Since it doesn’t touch Apple code, that seems like something that should go through the open source process instead of Apple’s, doesn’t it?

I was referring more to the request to enable the same thing on encode, which seems like something we would want to do in concert with this. If that were the case, we would need to make some internal NSJSONSerialization changes, too.

For anyone else needing this, I ended up working around this limitation by wrapping the values in arrays. I would really like to see support for top-level fragments so I can remove this hack.

1 Like

Using a similar workaround as @sindresorhus here, would love to get rid of it!