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

1 Like

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

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

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.

2 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.

2 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.

Terms of Service

Privacy Policy

Cookie Policy