Skip to content

Commit 12b4635

Browse files
authored
Add SCLF-0001 document (#1301)
1 parent 79b80a8 commit 12b4635

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Key Strategies for JSONEncoder and JSONDecoder
2+
3+
* Proposal: SCLF-0001
4+
* Author(s): Tony Parker <anthony.parker@apple.com>
5+
6+
##### Related radars or Swift bugs
7+
8+
* <rdar://problem/33019707> Snake case / Camel case conversions for JSONEncoder/Decoder
9+
10+
##### Revision history
11+
12+
* **v1** Initial version
13+
14+
## Introduction
15+
16+
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.
17+
18+
## Proposed solution
19+
20+
`JSONEncoder` and `JSONDecoder` will gain new strategy properties to allow for conversion of keys during encoding and decoding.
21+
22+
```swift
23+
class JSONDecoder {
24+
/// The strategy to use for automatically changing the value of keys before decoding.
25+
public enum KeyDecodingStrategy {
26+
/// Use the keys specified by each type. This is the default strategy.
27+
case useDefaultKeys
28+
29+
/// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.
30+
///
31+
/// 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.
32+
///
33+
/// Converting from snake case to camel case:
34+
/// 1. Capitalizes the word starting after each `_`
35+
/// 2. Removes all `_`
36+
/// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
37+
/// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
38+
///
39+
/// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
40+
case convertFromSnakeCase
41+
42+
/// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
43+
/// 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.
44+
case custom(([CodingKey]) -> CodingKey)
45+
}
46+
47+
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
48+
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys
49+
}
50+
51+
class JSONEncoder {
52+
/// The strategy to use for automatically changing the value of keys before encoding.
53+
public enum KeyEncodingStrategy {
54+
/// Use the keys specified by each type. This is the default strategy.
55+
case useDefaultKeys
56+
57+
/// Convert from "camelCaseKeys" to "snake_case_keys" before writing a key to JSON payload.
58+
///
59+
/// Capital characters are determined by testing membership in `CharacterSet.uppercaseLetters` and `CharacterSet.lowercaseLetters` (Unicode General Categories Lu and Lt).
60+
/// 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.
61+
///
62+
/// Converting from camel case to snake case:
63+
/// 1. Splits words at the boundary of lower-case to upper-case
64+
/// 2. Inserts `_` between words
65+
/// 3. Lowercases the entire string
66+
/// 4. Preserves starting and ending `_`.
67+
///
68+
/// For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
69+
///
70+
/// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted.
71+
case convertToSnakeCase
72+
73+
/// Provide a custom conversion to the key in the encoded JSON from the keys specified by the encoded types.
74+
/// 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.
75+
case custom(([CodingKey]) -> CodingKey)
76+
}
77+
78+
79+
/// The strategy to use for encoding keys. Defaults to `.useDefaultKeys`.
80+
open var keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys
81+
}
82+
```
83+
84+
## Detailed design
85+
86+
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.
87+
88+
Converting from snake case to camel case:
89+
90+
1. Capitalizes the word starting after each `_`
91+
2. Removes all `_`
92+
3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
93+
94+
For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
95+
96+
Converting from camel case to snake case:
97+
98+
1. Splits words at the boundary of lower-case to upper-case
99+
2. Inserts `_` between words
100+
3. Lowercases the entire string
101+
4. Preserves starting and ending `_`.
102+
103+
For example, `oneTwoThree` becomes `one_two_three`. `_oneTwoThree_` becomes `_one_two_three_`.
104+
105+
We also provide a `custom` action for both encoding and decoding to allow for maximum flexibility if the built-in options are not sufficient.
106+
107+
## Example
108+
109+
Given this JSON:
110+
111+
```
112+
{ "hello_world" : 3, "goodbye_cruel_world" : 10, "key" : 42 }
113+
```
114+
115+
Previously, you would customize your `Decodable` type with custom keys, like this:
116+
117+
```swift
118+
struct Thing : Decodable {
119+
120+
let helloWorld : Int
121+
let goodbyeCruelWorld: Int
122+
let key: Int
123+
124+
private enum CodingKeys : CodingKey {
125+
case helloWorld = "hello_world"
126+
case goodbyeCruelWorld = "goodbye_cruel_world"
127+
case key
128+
}
129+
}
130+
131+
var decoder = JSONDecoder()
132+
let result = try! decoder.decode(Thing.self, from: data)
133+
```
134+
135+
With this change, you can write much less boilerplate:
136+
137+
```swift
138+
struct Thing : Decodable {
139+
140+
let helloWorld : Int
141+
let goodbyeCruelWorld: Int
142+
let key: Int
143+
}
144+
145+
var decoder = JSONDecoder()
146+
decoder.keyDecodingStrategy = .convertFromSnakeCase
147+
let result = try! decoder.decode(Thing.self, from: data)
148+
```
149+
150+
## Alternatives considered
151+
152+
None.

0 commit comments

Comments
 (0)