[swift-evolution] JSONEncoder: Key strategies

Itai Ferber iferber at apple.com
Tue Nov 7 11:20:29 CST 2017


Hi Norio,

There are two reasons that I think this is valuable over doing something 
in `CodingKeys`:

1. The definition you give your coding keys affects all encoding 
formats. JSON is a format where snake_case can be relatively common, so 
the transformation makes a lot of sense there. For other formats, like 
plist files or otherwise, the transformation might not make as much 
sense. Instead of affecting all of your coding keys globally, this 
limits it to JSON.
2. More importantly, this allows you to transform keys of things which 
you don’t necessarily own. If you’re working with types that you 
didn’t write (but which are expected to have snake_case keys 
nonetheless), this allows you to perform that transformation. If this 
were instead an annotation on `CodingKeys` directly, you wouldn’t be 
able to perform it on types you don’t directly own.

— Itai

On 6 Nov 2017, at 17:39, Norio Nomura via swift-evolution wrote:

> Hi Tony,
>
> Is it better for us to choose on `Codable` side whether `rawValue` of
> `CodingKeys` should be generated with snake_case?
> It seems to be more consistent with the current method of setting
> `rawValue` of `CodingKeys` on `Codable` side.
>
> Thanks,
> --
> @norio_nomura
>
> 2017-11-07 5:54 GMT+09:00 Tony Parker via swift-evolution <
> swift-evolution at swift.org>:
>
>> Hi everyone,
>>
>> While we have no formal process at this time for proposals of changes 
>> to
>> Foundation-only code, I would still like to post one that we have run
>> through our internal process here for additional public comment.
>>
>> Link to PR with proposal content:
>>
>> https://github.com/apple/swift-corelibs-foundation/pull/1301
>>
>> Link to implementation for the overlay:
>>
>> https://github.com/apple/swift/pull/12779
>>
>> Markdown follows.
>>
>> Thanks,
>> - Tony
>>
>> # Key Strategies for JSONEncoder and JSONDecoder
>>
>> * Proposal: SCLF-0001
>> * Author(s): Tony Parker <anthony.parker at apple.com>
>>
>> ##### Related radars or Swift bugs
>>
>> * <rdar://problem/33019707> Snake case / Camel case conversions for
>> JSONEncoder/Decoder
>>
>> ##### Revision history
>>
>> * **v1** Initial version
>>
>> ## Introduction
>>
>> While early feedback for `JSONEncoder` and `JSONDecoder` has been 
>> very
>> positive, many developers have told us that they would appreciate a
>> convenience for converting between `snake_case_keys` and 
>> `camelCaseKeys`
>> without having to manually specify the key values for all types.
>>
>> ## Proposed solution
>>
>> `JSONEncoder` and `JSONDecoder` will gain new strategy properties to 
>> allow
>> for conversion of keys during encoding and decoding.
>>
>> ```swift
>> class JSONDecoder {
>>     /// The strategy to use for automatically changing the value of 
>> keys
>> before decoding.
>>     public enum KeyDecodingStrategy {
>>         /// Use the keys specified by each type. This is the default
>> strategy.
>>         case useDefaultKeys
>>
>>         /// Convert from "snake_case_keys" to "camelCaseKeys" before
>> attempting to match a key with the one specified by each type.
>>         ///
>>         /// The conversion to upper case uses `Locale.system`, also 
>> known
>> as the ICU "root" locale. This means the result is consistent 
>> regardless of
>> the current user's locale and language preferences.
>>         ///
>>         /// Converting from snake case to camel case:
>>         /// 1. Capitalizes the word starting after each `_`
>>         /// 2. Removes all `_`
>>         /// 3. Preserves starting and ending `_` (as these are often 
>> used
>> to indicate private variables or other metadata).
>>         /// For example, `one_two_three` becomes `oneTwoThree`.
>> `_one_two_three_` becomes `_oneTwoThree_`.
>>         ///
>>         /// - Note: Using a key decoding strategy has a nominal
>> performance cost, as each string key has to be inspected for the `_`
>> character.
>>         case convertFromSnakeCase
>>
>>         /// Provide a custom conversion from the key in the encoded 
>> JSON
>> to the keys specified by the decoded types.
>>         /// The full path to the current decoding position is 
>> provided for
>> context (in case you need to locate this key within the payload). The
>> returned key is used in place of the last component in the coding 
>> path
>> before decoding.
>>         case custom(([CodingKey]) -> CodingKey)
>>     }
>>
>>     /// The strategy to use for decoding keys. Defaults to
>> `.useDefaultKeys`.
>>     open var keyDecodingStrategy: KeyDecodingStrategy = 
>> .useDefaultKeys
>> }
>>
>> class JSONEncoder {
>>     /// The strategy to use for automatically changing the value of 
>> keys
>> before encoding.
>>     public enum KeyEncodingStrategy {
>>         /// Use the keys specified by each type. This is the default
>> strategy.
>>         case useDefaultKeys
>>
>>         /// Convert from "camelCaseKeys" to "snake_case_keys" before
>> writing a key to JSON payload.
>>         ///
>>         /// Capital characters are determined by testing membership 
>> in
>> `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters`
>> (Unicode General Categories Lu and Lt).
>>         /// The conversion to lower case uses `Locale.system`, also 
>> known
>> as the ICU "root" locale. This means the result is consistent 
>> regardless of
>> the current user's locale and language preferences.
>>         ///
>>         /// Converting from camel case to snake case:
>>         /// 1. Splits words at the boundary of lower-case to 
>> upper-case
>>         /// 2. Inserts `_` between words
>>         /// 3. Lowercases the entire string
>>         /// 4. Preserves starting and ending `_`.
>>         ///
>>         /// For example, `oneTwoThree` becomes `one_two_three`.
>> `_oneTwoThree_` becomes `_one_two_three_`.
>>         ///
>>         /// - Note: Using a key encoding strategy has a nominal
>> performance cost, as each string key has to be converted.
>>         case convertToSnakeCase
>>
>>         /// Provide a custom conversion to the key in the encoded 
>> JSON
>> from the keys specified by the encoded types.
>>         /// The full path to the current encoding position is 
>> provided for
>> context (in case you need to locate this key within the payload). The
>> returned key is used in place of the last component in the coding 
>> path
>> before encoding.
>>         case custom(([CodingKey]) -> CodingKey)
>>     }
>>
>>
>>     /// The strategy to use for encoding keys. Defaults to
>> `.useDefaultKeys`.
>>     open var keyEncodingStrategy: KeyEncodingStrategy = 
>> .useDefaultKeys
>> }
>> ```
>>
>> ## Detailed design
>>
>> The strategy enum allows developers to pick from common actions of
>> converting to and from `snake_case` to the Swift-standard 
>> `camelCase`. The
>> implementation is intentionally simple, because we want to make the 
>> rules
>> predictable.
>>
>> Converting from snake case to camel case:
>>
>> 1. Capitalizes the word starting after each `_`
>> 2. Removes all `_`
>> 3. Preserves starting and ending `_` (as these are often used to 
>> indicate
>> private variables or other metadata).
>>
>> For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_`
>> becomes `_oneTwoThree_`.
>>
>> Converting from camel case to snake case:
>>
>> 1. Splits words at the boundary of lower-case to upper-case
>> 2. Inserts `_` between words
>> 3. Lowercases the entire string
>> 4. Preserves starting and ending `_`.
>>
>> For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_`
>> becomes `_one_two_three_`.
>>
>> We also provide a `custom` action for both encoding and decoding to 
>> allow
>> for maximum flexibility if the built-in options are not sufficient.
>>
>> ## Example
>>
>> Given this JSON:
>>
>> ```
>> { "hello_world" : 3, "goodbye_cruel_world" : 10, "key" : 42 }
>> ```
>>
>> Previously, you would customize your `Decodable` type with custom 
>> keys,
>> like this:
>>
>> ```swift
>> struct Thing : Decodable {
>>
>>     let helloWorld : Int
>>     let goodbyeCruelWorld: Int
>>     let key: Int
>>
>>     private enum CodingKeys : CodingKey {
>>         case helloWorld = "hello_world"
>>         case goodbyeCruelWorld = "goodbye_cruel_world"
>>         case key
>>     }
>> }
>>
>> var decoder = JSONDecoder()
>> let result = try! decoder.decode(Thing.self, from: data)
>> ```
>>
>> With this change, you can write much less boilerplate:
>>
>> ```swift
>> struct Thing : Decodable {
>>
>>     let helloWorld : Int
>>     let goodbyeCruelWorld: Int
>>     let key: Int
>> }
>>
>> var decoder = JSONDecoder()
>> decoder.keyDecodingStrategy = .convertFromSnakeCase
>> let result = try! decoder.decode(Thing.self, from: data)
>> ```
>>
>> ## Alternatives considered
>>
>> None.
>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>


> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171107/e1ff3913/attachment.html>


More information about the swift-evolution mailing list