Skip to content

Commit bbc5485

Browse files
committed
SR-9828: JSONEncoder keyEncodingStrategy does not work on Linux
- Implement .convertToSnakeCase key encoding. (cherry picked from commit c0feda6)
1 parent 5acae43 commit bbc5485

File tree

2 files changed

+111
-23
lines changed

2 files changed

+111
-23
lines changed

Foundation/JSONEncoder.swift

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -383,30 +383,44 @@ fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingCon
383383
self.container = container
384384
}
385385

386+
// MARK: - Coding Path Operations
387+
388+
private func _converted(_ key: CodingKey) -> CodingKey {
389+
switch encoder.options.keyEncodingStrategy {
390+
case .useDefaultKeys:
391+
return key
392+
case .convertToSnakeCase:
393+
let newKeyString = JSONEncoder.KeyEncodingStrategy._convertToSnakeCase(key.stringValue)
394+
return _JSONKey(stringValue: newKeyString, intValue: key.intValue)
395+
case .custom(let converter):
396+
return converter(codingPath + [key])
397+
}
398+
}
399+
386400
// MARK: - KeyedEncodingContainerProtocol Methods
387401

388-
public mutating func encodeNil(forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = NSNull() }
389-
public mutating func encode(_ value: Bool, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
390-
public mutating func encode(_ value: Int, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
391-
public mutating func encode(_ value: Int8, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
392-
public mutating func encode(_ value: Int16, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
393-
public mutating func encode(_ value: Int32, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
394-
public mutating func encode(_ value: Int64, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
395-
public mutating func encode(_ value: UInt, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
396-
public mutating func encode(_ value: UInt8, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
397-
public mutating func encode(_ value: UInt16, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
398-
public mutating func encode(_ value: UInt32, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
399-
public mutating func encode(_ value: UInt64, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
400-
public mutating func encode(_ value: String, forKey key: Key) throws { self.container[key.stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
402+
public mutating func encodeNil(forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = NSNull() }
403+
public mutating func encode(_ value: Bool, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
404+
public mutating func encode(_ value: Int, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
405+
public mutating func encode(_ value: Int8, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
406+
public mutating func encode(_ value: Int16, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
407+
public mutating func encode(_ value: Int32, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
408+
public mutating func encode(_ value: Int64, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
409+
public mutating func encode(_ value: UInt, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
410+
public mutating func encode(_ value: UInt8, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
411+
public mutating func encode(_ value: UInt16, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
412+
public mutating func encode(_ value: UInt32, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
413+
public mutating func encode(_ value: UInt64, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
414+
public mutating func encode(_ value: String, forKey key: Key) throws { self.container[_converted(key).stringValue._bridgeToObjectiveC()] = self.encoder.box(value) }
401415

402416
public mutating func encode(_ value: Float, forKey key: Key) throws {
403417
// Since the float may be invalid and throw, the coding path needs to contain this key.
404418
self.encoder.codingPath.append(key)
405419
defer { self.encoder.codingPath.removeLast() }
406420
#if DEPLOYMENT_RUNTIME_SWIFT
407-
self.container[key.stringValue._bridgeToObjectiveC()] = try self.encoder.box(value)
421+
self.container[_converted(key).stringValue._bridgeToObjectiveC()] = try self.encoder.box(value)
408422
#else
409-
self.container[key.stringValue] = try self.encoder.box(value)
423+
self.container[_converted(key).stringValue] = try self.encoder.box(value)
410424
#endif
411425
}
412426

@@ -415,28 +429,28 @@ fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingCon
415429
self.encoder.codingPath.append(key)
416430
defer { self.encoder.codingPath.removeLast() }
417431
#if DEPLOYMENT_RUNTIME_SWIFT
418-
self.container[key.stringValue._bridgeToObjectiveC()] = try self.encoder.box(value)
432+
self.container[_converted(key).stringValue._bridgeToObjectiveC()] = try self.encoder.box(value)
419433
#else
420-
self.container[key.stringValue] = try self.encoder.box(value)
434+
self.container[_converted(key).stringValue] = try self.encoder.box(value)
421435
#endif
422436
}
423437

424438
public mutating func encode<T : Encodable>(_ value: T, forKey key: Key) throws {
425439
self.encoder.codingPath.append(key)
426440
defer { self.encoder.codingPath.removeLast() }
427441
#if DEPLOYMENT_RUNTIME_SWIFT
428-
self.container[key.stringValue._bridgeToObjectiveC()] = try self.encoder.box(value)
442+
self.container[_converted(key).stringValue._bridgeToObjectiveC()] = try self.encoder.box(value)
429443
#else
430-
self.container[key.stringValue] = try self.encoder.box(value)
444+
self.container[_converted(key).stringValue] = try self.encoder.box(value)
431445
#endif
432446
}
433447

434448
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
435449
let dictionary = NSMutableDictionary()
436450
#if DEPLOYMENT_RUNTIME_SWIFT
437-
self.container[key.stringValue._bridgeToObjectiveC()] = dictionary
451+
self.container[_converted(key).stringValue._bridgeToObjectiveC()] = dictionary
438452
#else
439-
self.container[key.stringValue] = dictionary
453+
self.container[_converted(key).stringValue] = dictionary
440454
#endif
441455

442456
self.codingPath.append(key)
@@ -449,9 +463,9 @@ fileprivate struct _JSONKeyedEncodingContainer<K : CodingKey> : KeyedEncodingCon
449463
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
450464
let array = NSMutableArray()
451465
#if DEPLOYMENT_RUNTIME_SWIFT
452-
self.container[key.stringValue._bridgeToObjectiveC()] = array
466+
self.container[_converted(key).stringValue._bridgeToObjectiveC()] = array
453467
#else
454-
self.container[key.stringValue] = array
468+
self.container[_converted(key).stringValue] = array
455469
#endif
456470

457471
self.codingPath.append(key)

TestFoundation/TestJSONEncoder.swift

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,79 @@ class TestJSONEncoder : XCTestCase {
499499
}
500500
}
501501

502+
func test_snake_case_encoding() throws {
503+
struct MyTestData: Codable, Equatable {
504+
let thisIsAString: String
505+
let thisIsABool: Bool
506+
let thisIsAnInt: Int
507+
let thisIsAnInt8: Int8
508+
let thisIsAnInt16: Int16
509+
let thisIsAnInt32: Int32
510+
let thisIsAnInt64: Int64
511+
let thisIsAUint: UInt
512+
let thisIsAUint8: UInt8
513+
let thisIsAUint16: UInt16
514+
let thisIsAUint32: UInt32
515+
let thisIsAUint64: UInt64
516+
let thisIsAFloat: Float
517+
let thisIsADouble: Double
518+
let thisIsADate: Date
519+
let thisIsAnArray: Array<Int>
520+
let thisIsADictionary: Dictionary<String, Bool>
521+
}
522+
523+
let data = MyTestData(thisIsAString: "Hello",
524+
thisIsABool: true,
525+
thisIsAnInt: 1,
526+
thisIsAnInt8: 2,
527+
thisIsAnInt16: 3,
528+
thisIsAnInt32: 4,
529+
thisIsAnInt64: 5,
530+
thisIsAUint: 6,
531+
thisIsAUint8: 7,
532+
thisIsAUint16: 8,
533+
thisIsAUint32: 9,
534+
thisIsAUint64: 10,
535+
thisIsAFloat: 11,
536+
thisIsADouble: 12,
537+
thisIsADate: Date.init(timeIntervalSince1970: 0),
538+
thisIsAnArray: [1, 2, 3],
539+
thisIsADictionary: [ "trueValue": true, "falseValue": false]
540+
)
541+
542+
let encoder = JSONEncoder()
543+
encoder.keyEncodingStrategy = .convertToSnakeCase
544+
encoder.dateEncodingStrategy = .iso8601
545+
let encodedData = try encoder.encode(data)
546+
guard let jsonObject = try JSONSerialization.jsonObject(with: encodedData) as? [String: Any] else {
547+
XCTFail("Cant decode json object")
548+
return
549+
}
550+
XCTAssertEqual(jsonObject["this_is_a_string"] as? String, "Hello")
551+
XCTAssertEqual(jsonObject["this_is_a_bool"] as? Bool, true)
552+
XCTAssertEqual(jsonObject["this_is_an_int"] as? Int, 1)
553+
XCTAssertEqual(jsonObject["this_is_an_int8"] as? Int8, 2)
554+
XCTAssertEqual(jsonObject["this_is_an_int16"] as? Int16, 3)
555+
XCTAssertEqual(jsonObject["this_is_an_int32"] as? Int32, 4)
556+
XCTAssertEqual(jsonObject["this_is_an_int64"] as? Int64, 5)
557+
XCTAssertEqual(jsonObject["this_is_a_uint"] as? UInt, 6)
558+
XCTAssertEqual(jsonObject["this_is_a_uint8"] as? UInt8, 7)
559+
XCTAssertEqual(jsonObject["this_is_a_uint16"] as? UInt16, 8)
560+
XCTAssertEqual(jsonObject["this_is_a_uint32"] as? UInt32, 9)
561+
XCTAssertEqual(jsonObject["this_is_a_uint64"] as? UInt64, 10)
562+
XCTAssertEqual(jsonObject["this_is_a_float"] as? Float, 11)
563+
XCTAssertEqual(jsonObject["this_is_a_double"] as? Double, 12)
564+
XCTAssertEqual(jsonObject["this_is_a_date"] as? String, "1970-01-01T00:00:00Z")
565+
XCTAssertEqual(jsonObject["this_is_an_array"] as? [Int], [1, 2, 3])
566+
XCTAssertEqual(jsonObject["this_is_a_dictionary"] as? [String: Bool], ["true_value": true, "false_value": false ])
567+
568+
let decoder = JSONDecoder()
569+
decoder.keyDecodingStrategy = .convertFromSnakeCase
570+
decoder.dateDecodingStrategy = .iso8601
571+
let decodedData = try decoder.decode(MyTestData.self, from: encodedData)
572+
XCTAssertEqual(data, decodedData)
573+
}
574+
502575
// MARK: - Helper Functions
503576
private var _jsonEmptyDictionary: Data {
504577
return "{}".data(using: .utf8)!
@@ -1089,6 +1162,7 @@ extension TestJSONEncoder {
10891162
("test_codingOfDouble", test_codingOfDouble),
10901163
("test_codingOfString", test_codingOfString),
10911164
("test_codingOfURL", test_codingOfURL),
1165+
("test_snake_case_encoding", test_snake_case_encoding),
10921166
]
10931167
}
10941168
}

0 commit comments

Comments
 (0)