Support nested custom CodingKeys for Codable types

Having the following json:

{
    "name": "Jude",
    "hobby": {
        "category": {
            "domain": "football"
        }
    }
}

It would be very cool to have a model like so (inspired by JSONModel):

struct Person: Codable {
    let name: String
    let hobby: String

    enum CodingKeys: String, CodingKey {
        case name
        case hobby = "hobby.category.domain"
    }
}

This can easily(:crossed_fingers:) be implemented by taking advantage of NSDictionary's valueForKeyPath existing implementation (which can even handle nested arrays out of the box); and NSDictionary is already used in JSONEncoder.swift.

If this proposal gets the green flag, I volunteer to take a stab at implementing it. In which case I'd appreciate if I can get a mentor on this, because I've looked through the repos, and the various documentation, as well as the wider web, but this endeavor still is daunting for me. I've cloned the repos and did initial builds with ninja and Xcode, but I have yet to find an effective and nimble development workflow.

P.S. In case of a GO, would PlistEncoder.swift also need to be adapted? I see that the 2 files already differ, the JSONEncoder one being much larger

2 Likes

How is it supposed to work with key that contains dot but are not nested key ?

{
    "name": "Jude",
    "hobby.category": {
        "domain": "football"
    }
}
1 Like

I agree with the motivation here. Getting values out of nested JSON dicts is unwieldy, requiring either a bunch of types or manually implementing Codable.

But I don't think this is the right solution. @Jean-Daniel's point basically rules out any form of separator in the coding key string value. Even using another form (tuple of strings or whatever), would still mean a big shift in the semantics of a CodingKey.

Imo, it would be better to keep the Codable API simple and wait on features like this until we have tools like annotations/atttributes and a more customizable synthesis mechanism.

In the meantime, If I had extensive need for this, I'd probably use Sourcery to do more customizable codable synthesis for me, tacking on whatever feature I need. Sourcery supports annotations in comments, so depending on your template, you could make it look something like this:

// sourcery: makeCodablePleaseThankYou
struct Person {
    let name: String
    // sourcery: nestedKeys = ["hobby", "category", "domain"]
    let hobby: String
}

If you are sure you'll never have dots in your keys, you could of course also make your template use a dot-separated string (//sourcery: nestedKeys = "hobby.category.domain")

5 Likes

Good point @Jean-Daniel, and thanks for the feedback @ahti (I'll check out Sourcery). Two possible solutions to the "key-that-contains-dot-problem":

  1. Easiest one is fail parsing. Not nice but even so, the overall usefulness of the feature beats this edge case imo (note! this would only fail in case of nested paths containing dots, like in the above example. For simple keys containing dots that exactly match the CodingKey this would still work).
  2. Make the implementation smarter by handling this case. This would require more effort.

In either case, priority would be given to keys containing dots over nested keys in case of an overlap.

We already have JSONDecoder.KeyDecodingStrategy, so I'm sure this could be fixed somehow with sensible defaults.

There are few considerable points:

  1. I believe that level of nesting shows design flaws in your server's API. It's weird to have simply a string nested few levels. If there are data which you don't need, that means your server returns useless data. In both cases you should consider redesigning you api.

  2. Dot(.) is a legitimate character in JSON keys. This proposal, while solves a problem which have a workaround, would introduce another issue for other scenarios! That means it may break some current implementations based on JSONDecoder's current behavior.

  3. JSONSerialization is an implementation detail. This proposal may (or may not) limit Swift community if they decide to replace it with a more capable library in future.

4 Likes

This is a tough point since one doesn't necessarily have access to the service. It might as well be a 3rd party service, contain other data as well which justifies the structure (but is unnecessary in the client implementation) or have another reason for needing to be structured the way it is.

4 Likes

For the given simple example, I don't terribly mind to write like below:

struct Person: Codable {
    struct Hobby: Codable {
        struct Category: Codable {
            let domain: String
        }
        let category: Category
    }
    let name: String
    let hobby: Hobby
}

But I wish I could declare a type without its name like below for brevity:

struct Person: Codable {
    let name: String
    let hobby: struct _: Codable {
        let category: struct _: Codable {
            let domain: String
        }
    }
}

And if you develop this wish towards more brevity, you'd arrive at what was suggested initially :slight_smile:

Plus, with the proper implementation, it can take care of any of the above scenarios. Its simplicity and effectiveness would be a Swiss Army knife in the realm of JSON parsing.

As far as the form it would take, the tuple (or rather array, since you can't know how many elements it could contain) approach also seems interesting.

What is the normal lifecycle of a pitch? If it doesn't receive enough activity, does it remain stale indefinitely, or does a senior member of the community eventually pronounce upon it?

Senior members are not guaranteed to even look at it until a pull request is submitted to GitHub - apple/swift-evolution: This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. and an accompanying implementation.

A proposal is unlikely to be scheduled for review if the junior members have not shown much interest or if those who support it have not coalesced around a single implementation yet. Those are reasonable indicators of whether it will be worth your effort to put together a complete proposal and implementation. Proposals that have already undergone review are listed in the swift-evolution repository, and many of them have links to the pitch threads that spawned them, so you can follow those links to get an idea of how they fared as a pitch according to the same metrics.

2 Likes