diff --git a/Sources/FoundationEssentials/JSON/JSONEncoder.swift b/Sources/FoundationEssentials/JSON/JSONEncoder.swift index 84159bbd1..046345fd8 100644 --- a/Sources/FoundationEssentials/JSON/JSONEncoder.swift +++ b/Sources/FoundationEssentials/JSON/JSONEncoder.swift @@ -1184,14 +1184,16 @@ private extension __JSONEncoder { } } - func wrap(_ dict: [String : Encodable], for additionalKey: (some CodingKey)? = _CodingKey?.none) throws -> JSONEncoderValue? { + func wrap(_ dict: _JSONCodingKeyRepresentableDictionaryEncodableMarker, for additionalKey: (some CodingKey)? = _CodingKey?.none) throws -> JSONEncoderValue? { + let dict = dict as! [AnyHashable: Encodable] var result = [String: JSONEncoderValue]() result.reserveCapacity(dict.count) let encoder = __JSONEncoder(options: self.options, ownerEncoder: self) for (key, value) in dict { - encoder.codingKey = _CodingKey(stringValue: key) - result[key] = try encoder.wrap(value) + let stringKey = (key.base as! CodingKeyRepresentable).codingKey.stringValue + encoder.codingKey = _CodingKey(stringValue: stringKey) + result[stringKey] = try encoder.wrap(value) } return .object(result) @@ -1214,8 +1216,8 @@ private extension __JSONEncoder { return self.wrap(url.absoluteString) } else if let decimal = value as? Decimal { return .number(decimal.description) - } else if let encodable = value as? _JSONStringDictionaryEncodableMarker { - return try self.wrap(encodable as! [String:Encodable], for: additionalKey) + } else if let encodable = value as? _JSONCodingKeyRepresentableDictionaryEncodableMarker { + return try self.wrap(encodable, for: additionalKey) } else if let array = value as? _JSONDirectArrayEncodable { if options.outputFormatting.contains(.prettyPrinted) { let (bytes, lengths) = try array.individualElementRepresentation(encoder: self, additionalKey) @@ -1362,11 +1364,11 @@ extension JSONEncoder : @unchecked Sendable {} // Special-casing Support //===----------------------------------------------------------------------===// -/// A marker protocol used to determine whether a value is a `String`-keyed `Dictionary` +/// A marker protocol used to determine whether a value is a `CodingKeyRepresentable`-keyed `Dictionary` /// containing `Encodable` values (in which case it should be exempt from key conversion strategies). -private protocol _JSONStringDictionaryEncodableMarker { } +private protocol _JSONCodingKeyRepresentableDictionaryEncodableMarker { } -extension Dictionary : _JSONStringDictionaryEncodableMarker where Key == String, Value: Encodable { } +extension Dictionary : _JSONCodingKeyRepresentableDictionaryEncodableMarker where Key: CodingKeyRepresentable, Value: Encodable { } /// A protocol used to determine whether a value is an `Array` containing values that allow /// us to bypass UnkeyedEncodingContainer overhead by directly encoding the contents as diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 566919ece..a2a76ca2e 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -2333,6 +2333,22 @@ extension JSONEncoderTests { #expect(expected == resultString) } + + @Test func encodingDictionaryCodingKeyRepresentableKeyConversionUntouched() throws { + struct Key: RawRepresentable, CodingKeyRepresentable, Hashable, Codable { + let rawValue: String + } + + let expected = "{\"leaveMeAlone\":\"test\"}" + let toEncode: [Key: String] = [Key(rawValue: "leaveMeAlone"): "test"] + + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + let resultData = try encoder.encode(toEncode) + let resultString = String(bytes: resultData, encoding: .utf8) + + #expect(expected == resultString) + } @Test func keyStrategySnakeGeneratedAndCustom() throws { // Test that this works with a struct that has automatically generated keys