<html><head><meta http-equiv="Content-Type" content="text/html; charset=us-ascii"></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; line-break: after-white-space;" class="">Hi everyone,<div class=""><br class=""></div><div class="">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.</div><div class=""><br class=""></div><div class="">Link to PR with proposal content:</div><div class=""><br class=""></div><div class=""><a href="https://github.com/apple/swift-corelibs-foundation/pull/1301" class="">https://github.com/apple/swift-corelibs-foundation/pull/1301</a></div><div class=""><br class=""></div><div class="">Link to implementation for the overlay:</div><div class=""><br class=""></div><div class=""><a href="https://github.com/apple/swift/pull/12779" class="">https://github.com/apple/swift/pull/12779</a></div><div class=""><br class=""></div><div class="">Markdown follows.</div><div class=""><br class=""></div><div class="">Thanks,</div><div class="">- Tony</div><div class=""><br class=""></div><div class=""><div class=""># Key Strategies for JSONEncoder and JSONDecoder</div><div class=""><br class=""></div><div class="">* Proposal: SCLF-0001</div><div class="">* Author(s): Tony Parker <<a href="mailto:anthony.parker@apple.com" class="">anthony.parker@apple.com</a>></div><div class=""><br class=""></div><div class="">##### Related radars or Swift bugs</div><div class=""><br class=""></div><div class="">* <<a href="rdar://problem/33019707" class="">rdar://problem/33019707</a>> Snake case / Camel case conversions for JSONEncoder/Decoder</div><div class=""><br class=""></div><div class="">##### Revision history</div><div class=""><br class=""></div><div class="">* **v1** Initial version</div><div class=""><br class=""></div><div class="">## Introduction</div><div class=""><br class=""></div><div class="">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.</div><div class=""><br class=""></div><div class="">## Proposed solution</div><div class=""><br class=""></div><div class="">`JSONEncoder` and `JSONDecoder` will gain new strategy properties to allow for conversion of keys during encoding and decoding.</div><div class=""><br class=""></div><div class="">```swift</div><div class="">class JSONDecoder {</div><div class=""> /// The strategy to use for automatically changing the value of keys before decoding.</div><div class=""> public enum KeyDecodingStrategy {</div><div class=""> /// Use the keys specified by each type. This is the default strategy.</div><div class=""> case useDefaultKeys</div><div class=""> </div><div class=""> /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.</div><div class=""> /// </div><div class=""> /// 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.</div><div class=""> ///</div><div class=""> /// Converting from snake case to camel case:</div><div class=""> /// 1. Capitalizes the word starting after each `_`</div><div class=""> /// 2. Removes all `_`</div><div class=""> /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).</div><div class=""> /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.</div><div class=""> ///</div><div class=""> /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.</div><div class=""> case convertFromSnakeCase</div><div class=""> </div><div class=""> /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.</div><div class=""> /// 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.</div><div class=""> case custom(([CodingKey]) -> CodingKey)</div><div class=""> }</div><div class=""> </div><div class=""> /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.</div><div class=""> open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys</div><div class="">}</div><div class=""><br class=""></div><div class="">class JSONEncoder {</div><div class=""> /// The strategy to use for automatically changing the value of keys before encoding.</div><div class=""> public enum KeyEncodingStrategy {</div><div class=""> /// Use the keys specified by each type. This is the default strategy.</div><div class=""> case useDefaultKeys</div><div class=""> </div><div class=""> /// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload.</div><div class=""> ///</div><div class=""> /// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).</div><div class=""> /// 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.</div><div class=""> ///</div><div class=""> /// Converting from camel case to snake case:</div><div class=""> /// 1. Splits words at the boundary of lower-case to upper-case</div><div class=""> /// 2. Inserts `_` between words</div><div class=""> /// 3. Lowercases the entire string</div><div class=""> /// 4. Preserves starting and ending `_`.</div><div class=""> ///</div><div class=""> /// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.</div><div class=""> ///</div><div class=""> /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.</div><div class=""> case convertToSnakeCase</div><div class=""> </div><div class=""> /// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.</div><div class=""> /// 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.</div><div class=""> case custom(([CodingKey]) -> CodingKey)</div><div class=""> }</div><div class=""> </div><div class=""> </div><div class=""> /// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.</div><div class=""> open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys</div><div class="">}</div><div class="">```</div><div class=""><br class=""></div><div class="">## Detailed design</div><div class=""><br class=""></div><div class="">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.</div><div class=""><br class=""></div><div class="">Converting from snake case to camel case:</div><div class=""><br class=""></div><div class="">1. Capitalizes the word starting after each `_`</div><div class="">2. Removes all `_`</div><div class="">3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).</div><div class=""><br class=""></div><div class="">For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.</div><div class=""><br class=""></div><div class="">Converting from camel case to snake case:</div><div class=""><br class=""></div><div class="">1. Splits words at the boundary of lower-case to upper-case</div><div class="">2. Inserts `_` between words</div><div class="">3. Lowercases the entire string</div><div class="">4. Preserves starting and ending `_`.</div><div class=""><br class=""></div><div class="">For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.</div><div class=""><br class=""></div><div class="">We also provide a `custom` action for both encoding and decoding to allow for maximum flexibility if the built-in options are not sufficient.</div><div class=""><br class=""></div><div class="">## Example</div><div class=""><br class=""></div><div class="">Given this JSON:</div><div class=""><br class=""></div><div class="">```</div><div class="">{ "hello_world" : 3, "goodbye_cruel_world" : 10, "key" : 42 }</div><div class="">```</div><div class=""><br class=""></div><div class="">Previously, you would customize your `Decodable` type with custom keys, like this:</div><div class=""><br class=""></div><div class="">```swift</div><div class="">struct Thing : Decodable {</div><div class=""><br class=""></div><div class=""> let helloWorld : Int</div><div class=""> let goodbyeCruelWorld: Int</div><div class=""> let key: Int</div><div class=""><br class=""></div><div class=""> private enum CodingKeys : CodingKey {</div><div class=""> case helloWorld = "hello_world"</div><div class=""> case goodbyeCruelWorld = "goodbye_cruel_world"</div><div class=""> case key</div><div class=""> }</div><div class="">}</div><div class=""><br class=""></div><div class="">var decoder = JSONDecoder()</div><div class="">let result = try! decoder.decode(Thing.self, from: data)</div><div class="">```</div><div class=""><br class=""></div><div class="">With this change, you can write much less boilerplate:</div><div class=""><br class=""></div><div class="">```swift</div><div class="">struct Thing : Decodable {</div><div class=""><br class=""></div><div class=""> let helloWorld : Int</div><div class=""> let goodbyeCruelWorld: Int</div><div class=""> let key: Int</div><div class="">}</div><div class=""><br class=""></div><div class="">var decoder = JSONDecoder()</div><div class="">decoder.keyDecodingStrategy = .convertFromSnakeCase</div><div class="">let result = try! decoder.decode(Thing.self, from: data)</div><div class="">```</div><div class=""><br class=""></div><div class="">## Alternatives considered</div><div class=""><br class=""></div><div class="">None.</div></div><div class=""><br class=""></div></body></html>