From d60a635197cfe3a36440a6768c3d94563fae3ee8 Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:43:52 +0200 Subject: [PATCH 01/19] Added better readability to a comment --- Source/SwiftyJSON/SwiftyJSON.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index e625810e..6e1b9ded 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -433,7 +433,7 @@ extension JSON { Example: - ``` + ```swift let json = JSON[data] let path = [9,"list","person","name"] let name = json[path] From c27a45e6386fc869d0e0f23eda1ef61104d2770e Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:49:02 +0200 Subject: [PATCH 02/19] Comments --- Source/SwiftyJSON/SwiftyJSON.swift | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 6e1b9ded..c8be53c2 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -467,10 +467,14 @@ extension JSON { Find a json in the complex data structures by using array of Int and/or String as path. - parameter path: The target json's path. Example: - + ```swift let name = json[9,"list","person","name"] - - The same as: let name = json[9]["list"]["person"]["name"] + ``` + + The same as: + ```swift + let name = json[9]["list"]["person"]["name"] + ``` - returns: Return a json found by the path or a null json with error */ From c8ae6e331dba0627181d0fd91e3c844070dd628a Mon Sep 17 00:00:00 2001 From: Will Jessop Date: Thu, 10 Aug 2023 10:57:32 -0400 Subject: [PATCH 03/19] Add info on removing dictionary keys in place --- README.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/README.md b/README.md index a4cd321f..0b3fc855 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Platform | Build Status - [Raw object](#raw-object) - [Literal convertibles](#literal-convertibles) - [Merging](#merging) + - [Removing elements](#removing-elements) 5. [Work with Alamofire](#work-with-alamofire) 6. [Work with Moya](#work-with-moya) 7. [SwiftyJSON Model Generator](#swiftyjson-model-generator) @@ -503,6 +504,69 @@ let updated = original.merge(with: update) // ] ``` + +#### Removing elements + +If you are storing dictionaries, you can remove elements using `dictionaryObject.removeValue(forKey:)`. This mutates the JSON object in place. + +For example: + +```swift +var object = JSON([ + "one": ["color": "blue"], + "two": ["city": "tokyo", + "country": "japan", + "foods": [ + "breakfast": "tea", + "lunch": "sushi" + ] + ] +]) +``` + +Lets remove the `country` key: + +```swift +object["two"].dictionaryObject?.removeValue(forKey: "country") +``` + +If you `print(object)`, you'll see that the `country` key no longer exists. + +```json +{ + "one" : { + "color" : "blue" + }, + "two" : { + "city" : "tokyo", + "foods" : { + "breakfast" : "tea", + "lunch" : "sushi" + } + } +} +``` + +This also works for nested dictionaries: + +```swift +object["two"]["foods"].dictionaryObject?.removeValue(forKey: "breakfast") +``` + +```json +{ + "one" : { + "color" : "blue" + }, + "two" : { + "city" : "tokyo", + "foods" : { + "lunch" : "sushi" + } + } +} +``` + ## String representation There are two options available: - use the default Swift one From be72f800ceb3ef6d2e618e2be50d37ad7e0e094f Mon Sep 17 00:00:00 2001 From: Blazej SLEBODA <5544365+Adobels@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:18:57 +0200 Subject: [PATCH 04/19] Fix missing PrivacyInfo.xcprivacy in xcodeproj file --- SwiftyJSON.xcodeproj/project.pbxproj | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/SwiftyJSON.xcodeproj/project.pbxproj b/SwiftyJSON.xcodeproj/project.pbxproj index 3f48deaa..ec223336 100644 --- a/SwiftyJSON.xcodeproj/project.pbxproj +++ b/SwiftyJSON.xcodeproj/project.pbxproj @@ -51,6 +51,10 @@ 9C459F041A9103C1008C9A41 /* DictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B66C8B19E51D6500540692 /* DictionaryTests.swift */; }; 9C459F051A9103C1008C9A41 /* ArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B66C8D19E52F4200540692 /* ArrayTests.swift */; }; 9C7DFC661A9102BD005AA3F7 /* SwiftyJSON.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C7DFC5B1A9102BD005AA3F7 /* SwiftyJSON.framework */; }; + A1DE64C62BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1DE64C52BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy */; }; + A1DE64C72BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1DE64C52BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy */; }; + A1DE64C82BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1DE64C52BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy */; }; + A1DE64C92BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A1DE64C52BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy */; }; A819C49719E1A7DD00ADCC3D /* LiteralConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A819C49619E1A7DD00ADCC3D /* LiteralConvertibleTests.swift */; }; A819C49919E1B10300ADCC3D /* RawRepresentableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A819C49819E1B10300ADCC3D /* RawRepresentableTests.swift */; }; A819C49F19E2EE5B00ADCC3D /* SubscriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A819C49E19E2EE5B00ADCC3D /* SubscriptTests.swift */; }; @@ -126,6 +130,7 @@ 9C459EF61A9103B1008C9A41 /* Info-macOS.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Info-macOS.plist"; path = "../Info-macOS.plist"; sourceTree = ""; }; 9C7DFC5B1A9102BD005AA3F7 /* SwiftyJSON.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftyJSON.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9C7DFC651A9102BD005AA3F7 /* SwiftyJSON macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SwiftyJSON macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + A1DE64C52BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = SwiftyJSON/PrivacyInfo.xcprivacy; sourceTree = ""; }; A819C49619E1A7DD00ADCC3D /* LiteralConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LiteralConvertibleTests.swift; path = ../SwiftJSONTests/LiteralConvertibleTests.swift; sourceTree = ""; }; A819C49819E1B10300ADCC3D /* RawRepresentableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RawRepresentableTests.swift; path = ../SwiftJSONTests/RawRepresentableTests.swift; sourceTree = ""; }; A819C49E19E2EE5B00ADCC3D /* SubscriptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SubscriptTests.swift; path = ../SwiftJSONTests/SubscriptTests.swift; sourceTree = ""; }; @@ -241,6 +246,7 @@ 2E4FEFDE19575BE100351305 /* Supporting Files */ = { isa = PBXGroup; children = ( + A1DE64C52BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy */, 2E4FEFDF19575BE100351305 /* Info-iOS.plist */, 030B6CDC1A6E171D00C2D4F1 /* Info-macOS.plist */, E4D7CCE91B9465A800EE7221 /* Info-watchOS.plist */, @@ -527,6 +533,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1DE64C62BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -542,6 +549,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1DE64C92BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -549,6 +557,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1DE64C72BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -572,6 +581,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1DE64C82BC7D95C0097BCE6 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; From ed2bedd6f804b0dd65b03b0cd68d9b2e9909130c Mon Sep 17 00:00:00 2001 From: Brandon Fraune <52302810+fraune@users.noreply.github.com> Date: Tue, 21 May 2024 12:39:35 -0500 Subject: [PATCH 05/19] Update .swiftlint.yml Resolve SwiftLint warning: `'variable_name' has been renamed to 'identifier_name' and will be completely removed in a future release.` --- .swiftlint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 03003d1d..36ee05ac 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,7 +1,7 @@ disabled_rules: - force_cast - force_try - - variable_name + - identifier_name - type_name - file_length - line_length From d0058a7b750f769b37e00de4e2546bec85ab55c0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 10 Aug 2024 19:21:22 +0200 Subject: [PATCH 06/19] Delete .travis.yml -- Travis CI is no longer free to open source https://app.travis-ci.com/github/SwiftyJSON --- .travis.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 17140ab1..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: objective-c -osx_image: xcode10.2 -xcode_sdk: iphonesimulator12.0 -script: -- set -o pipefail -- travis_retry xcodebuild -workspace SwiftyJSON.xcworkspace -scheme "SwiftyJSON iOS" -destination "platform=iOS Simulator,name=iPhone 6" build-for-testing test | xcpretty -- travis_retry xcodebuild -workspace SwiftyJSON.xcworkspace -scheme "SwiftyJSON macOS" build-for-testing test | xcpretty -- travis_retry xcodebuild -workspace SwiftyJSON.xcworkspace -scheme "SwiftyJSON tvOS" -destination "platform=tvOS Simulator,name=Apple TV" build-for-testing test | xcpretty From 06fcc37fb14d80d6130621bf6f9940f4d601023a Mon Sep 17 00:00:00 2001 From: Joe Ferrucci <2114494+JoeFerrucci@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:51:47 -0500 Subject: [PATCH 07/19] Remove spam URL URL is outdated and leads to spammy/phishy site through redirects. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index d87739bb..c92bb44e 100644 --- a/README.md +++ b/README.md @@ -556,5 +556,4 @@ provider.request(.showProducts) { result in ## SwiftyJSON Model Generator Tools to generate SwiftyJSON Models -* [JSON Cafe](http://www.jsoncafe.com/) * [JSON Export](https://github.com/Ahmed-Ali/JSONExport) From b00fb02693b50122ef83894afd1846074e572e6a Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Mon, 26 May 2025 22:30:16 +0200 Subject: [PATCH 08/19] perf: improve unwrap method performance by using native `mapValues` Around 10% better performance on tests, 20% on "real-world" usage. --- Source/SwiftyJSON/SwiftyJSON.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index c8be53c2..ec1822b9 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -265,11 +265,7 @@ private func unwrap(_ object: Any) -> Any { case let array as [Any]: return array.map(unwrap) case let dictionary as [String: Any]: - var d = dictionary - dictionary.forEach { pair in - d[pair.key] = unwrap(pair.value) - } - return d + return dictionary.mapValues(unwrap) default: return object } From 76f8d81bf1c300ddec2704cdc6fc2dd06b1313fd Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Mon, 26 May 2025 23:16:49 +0200 Subject: [PATCH 09/19] perf: improve dictionnary property getter by using native `mapValues` Around 8% better on tests. --- Source/SwiftyJSON/SwiftyJSON.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index ec1822b9..1da9e58f 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -705,11 +705,7 @@ extension JSON { //Optional [String : JSON] public var dictionary: [String: JSON]? { if type == .dictionary { - var d = [String: JSON](minimumCapacity: rawDictionary.count) - rawDictionary.forEach { pair in - d[pair.key] = JSON(pair.value) - } - return d + return rawDictionary.mapValues(JSON.init(_:)) } else { return nil } From 45c240c76951c27f0aa39d63bfc02ef02943a10b Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Mon, 26 May 2025 23:35:06 +0200 Subject: [PATCH 10/19] perf: improve performance of `dictionaryLiteral` initializer of JSON --- Source/SwiftyJSON/SwiftyJSON.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 1da9e58f..6e0904ac 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -524,7 +524,7 @@ extension JSON: Swift.ExpressibleByFloatLiteral { extension JSON: Swift.ExpressibleByDictionaryLiteral { public init(dictionaryLiteral elements: (String, Any)...) { - let dictionary = elements.reduce(into: [String: Any](), { $0[$1.0] = $1.1}) + let dictionary = Dictionary(elements, uniquingKeysWith: { $1 }) self.init(dictionary) } } From c9d0f9d8ccb5f68af4915ff6dafa55a1c17f1001 Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Mon, 26 May 2025 23:43:34 +0200 Subject: [PATCH 11/19] perf: improve performance of `array` property getter by removing use of closure in map function --- Source/SwiftyJSON/SwiftyJSON.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 6e0904ac..7b4ff42f 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -676,7 +676,7 @@ extension JSON { //Optional [JSON] public var array: [JSON]? { - return type == .array ? rawArray.map { JSON($0) } : nil + return type == .array ? rawArray.map(JSON.init(_:)) : nil } //Non-optional [JSON] From ae7ca8b3d404a1b24e23cb5c927ddc6cbdfbf7fa Mon Sep 17 00:00:00 2001 From: Antoine Bollengier <44288655+b5i@users.noreply.github.com> Date: Mon, 26 May 2025 23:53:49 +0200 Subject: [PATCH 12/19] perf: remove use of closure in `stringValue` property getter --- Source/SwiftyJSON/SwiftyJSON.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 7b4ff42f..97daf4fc 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -787,7 +787,7 @@ extension JSON { switch type { case .string: return object as? String ?? "" case .number: return rawNumber.stringValue - case .bool: return (object as? Bool).map { String($0) } ?? "" + case .bool: return (object as? Bool).map(String.init) ?? "" default: return "" } } From ef6d3592a29a1e3cc7c7a6f187523da7682d0799 Mon Sep 17 00:00:00 2001 From: wongzigii Date: Tue, 27 May 2025 15:32:20 +0800 Subject: [PATCH 13/19] Enable GitHub Action CI and fix tests --- .github/workflows/ci.yml | 25 ++++++++++++++++++++ Package.swift | 8 ++++++- Tests/SwiftJSONTests/BaseTests.swift | 4 ++-- Tests/SwiftJSONTests/PerformanceTests.swift | 4 ++-- Tests/SwiftJSONTests/SequenceTypeTests.swift | 8 ++++--- Tests/{Tes => SwiftJSONTests}/Tests.json | 0 6 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml rename Tests/{Tes => SwiftJSONTests}/Tests.json (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2680e910 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + build-and-test: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: '6.0' + + - name: Build + run: swift build --build-tests + + - name: Run tests + run: swift test \ No newline at end of file diff --git a/Package.swift b/Package.swift index 1fdaf06c..ba89fb5f 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,13 @@ let package = Package( .copy("PrivacyInfo.xcprivacy") ] ), - .testTarget(name: "SwiftJSONTests", dependencies: ["SwiftyJSON"]) + .testTarget( + name: "SwiftJSONTests", + dependencies: ["SwiftyJSON"], + resources: [ + .copy("Tests.json") + ] + ) ], swiftLanguageVersions: [.v5] ) diff --git a/Tests/SwiftJSONTests/BaseTests.swift b/Tests/SwiftJSONTests/BaseTests.swift index 9139c204..634e1c8b 100644 --- a/Tests/SwiftJSONTests/BaseTests.swift +++ b/Tests/SwiftJSONTests/BaseTests.swift @@ -33,8 +33,8 @@ class BaseTests: XCTestCase { // let file = "./Tests/Tes/Tests.json" // self.testData = try? Data(contentsOf: URL(fileURLWithPath: file)) - if let file = Bundle(for: BaseTests.self).path(forResource: "Tests", ofType: "json") { - self.testData = try? Data(contentsOf: URL(fileURLWithPath: file)) + if let file = Bundle.module.url(forResource: "Tests", withExtension: "json") { + self.testData = try? Data(contentsOf: file) } else { XCTFail("Can't find the test JSON file") } diff --git a/Tests/SwiftJSONTests/PerformanceTests.swift b/Tests/SwiftJSONTests/PerformanceTests.swift index 7535f7d8..70f1f2f2 100644 --- a/Tests/SwiftJSONTests/PerformanceTests.swift +++ b/Tests/SwiftJSONTests/PerformanceTests.swift @@ -30,8 +30,8 @@ class PerformanceTests: XCTestCase { override func setUp() { super.setUp() - if let file = Bundle(for: PerformanceTests.self).path(forResource: "Tests", ofType: "json") { - self.testData = try? Data(contentsOf: URL(fileURLWithPath: file)) + if let file = Bundle.module.url(forResource: "Tests", withExtension: "json") { + self.testData = try? Data(contentsOf: file) } else { XCTFail("Can't find the test JSON file") } diff --git a/Tests/SwiftJSONTests/SequenceTypeTests.swift b/Tests/SwiftJSONTests/SequenceTypeTests.swift index d0d8cadd..557c3dbb 100644 --- a/Tests/SwiftJSONTests/SequenceTypeTests.swift +++ b/Tests/SwiftJSONTests/SequenceTypeTests.swift @@ -25,10 +25,12 @@ import SwiftyJSON class SequenceTypeTests: XCTestCase { + var testData: Data? + func testJSONFile() { - if let file = Bundle(for: BaseTests.self).path(forResource: "Tests", ofType: "json") { - let testData = try? Data(contentsOf: URL(fileURLWithPath: file)) - guard let json = try? JSON(data: testData!) else { + if let file = Bundle.module.url(forResource: "Tests", withExtension: "json") { + self.testData = try? Data(contentsOf: file) + guard let json = try? JSON(data: self.testData!) else { XCTFail("Unable to parse the data") return } diff --git a/Tests/Tes/Tests.json b/Tests/SwiftJSONTests/Tests.json similarity index 100% rename from Tests/Tes/Tests.json rename to Tests/SwiftJSONTests/Tests.json From 9f751e8dca235d35954edebf943662dbf94d160c Mon Sep 17 00:00:00 2001 From: wongzigii Date: Tue, 27 May 2025 15:39:51 +0800 Subject: [PATCH 14/19] Update README CI Badge --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 58aeb989..e1740680 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ # SwiftyJSON +[![CI](https://github.com/SwiftyJSON/SwiftyJSON/actions/workflows/ci.yml/badge.svg)](https://github.com/SwiftyJSON/SwiftyJSON/actions/workflows/ci.yml) + [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![CocoaPods](https://img.shields.io/cocoapods/v/SwiftyJSON.svg) ![Platform](https://img.shields.io/badge/platforms-iOS%208.0%20%7C%20macOS%2010.10%20%7C%20tvOS%209.0%20%7C%20watchOS%203.0-F28D00.svg) [![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com) SwiftyJSON makes it easy to deal with JSON data in Swift. -Platform | Build Status ----------| --------------| -*OS | [![Travis CI](https://travis-ci.org/SwiftyJSON/SwiftyJSON.svg?branch=master)](https://travis-ci.org/SwiftyJSON/SwiftyJSON) | -[Linux](https://github.com/IBM-Swift/SwiftyJSON) | [![Build Status](https://travis-ci.org/IBM-Swift/SwiftyJSON.svg?branch=master)](https://travis-ci.org/IBM-Swift/SwiftyJSON) | - - 1. [Why is the typical JSON handling in Swift NOT good](#why-is-the-typical-json-handling-in-swift-not-good) 2. [Requirements](#requirements) 3. [Integration](#integration) From f02c453bb544085109a02f2f2fe8f2cfb0b7f6e8 Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Sun, 25 Jan 2026 21:14:51 -0700 Subject: [PATCH 15/19] Add Swift 6 support with Sendable conformance - Update Package.swift to swift-tools-version:6.0 - Remove swiftLanguageVersions lock - Add @unchecked Sendable to JSON struct - Add Sendable to Type, SwiftyJSONError, JSONKey enums - Add comprehensive concurrency tests (18 tests) - Update README requirements and CHANGELOG The JSON type can now safely cross actor boundaries in Swift 6 concurrency. Closes #1163 --- CHANGELOG.md | 9 + Package.swift | 5 +- README.md | 3 +- Source/SwiftyJSON/SwiftyJSON.swift | 8 +- Tests/SwiftJSONTests/ConcurrencyTests.swift | 386 ++++++++++++++++++++ 5 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 Tests/SwiftJSONTests/ConcurrencyTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a0935d3..082b0247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [Full Changelog](https://github.com/SwiftyJSON/SwiftyJSON/compare/2.2.0...HEAD) +### Added +- Swift 6 support with full Sendable conformance ([#1163](https://github.com/SwiftyJSON/SwiftyJSON/issues/1163)) +- Comprehensive concurrency tests for actor boundary validation + +### Changed +- `JSON` struct conforms to `@unchecked Sendable` +- `Type`, `SwiftyJSONError`, `JSONKey` enums conform to `Sendable` +- Minimum Swift tools version updated to 6.0 + **Closed issues:** - 156 compiler errors Mavericks + Xcode 6.2 [\#220](https://github.com/SwiftyJSON/SwiftyJSON/issues/220) diff --git a/Package.swift b/Package.swift index ba89fb5f..fb026e55 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:6.0 import PackageDescription let package = Package( @@ -20,6 +20,5 @@ let package = Package( .copy("Tests.json") ] ) - ], - swiftLanguageVersions: [.v5] + ] ) diff --git a/README.md b/README.md index e1740680..a6f3037a 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ if let userName = result.string { ## Requirements - iOS 8.0+ | macOS 10.10+ | tvOS 9.0+ | watchOS 2.0+ -- Xcode 8 +- Xcode 16+ +- Swift 6.0+ ## Integration diff --git a/Source/SwiftyJSON/SwiftyJSON.swift b/Source/SwiftyJSON/SwiftyJSON.swift index 97daf4fc..d2f39ef2 100644 --- a/Source/SwiftyJSON/SwiftyJSON.swift +++ b/Source/SwiftyJSON/SwiftyJSON.swift @@ -24,7 +24,7 @@ import Foundation // MARK: - Error // swiftlint:disable line_length -public enum SwiftyJSONError: Int, Swift.Error { +public enum SwiftyJSONError: Int, Swift.Error, Sendable { case unsupportedType = 999 case indexOutOfBounds = 900 case elementTooDeep = 902 @@ -67,7 +67,7 @@ JSON's type definitions. See http://www.json.org */ -public enum Type: Int { +public enum Type: Int, Sendable { case number case string case bool @@ -79,7 +79,7 @@ public enum Type: Int { // MARK: - JSON Base -public struct JSON { +public struct JSON: @unchecked Sendable { /** Creates a JSON using the data. @@ -339,7 +339,7 @@ extension JSON: Swift.Collection { /** * To mark both String and Int can be used in subscript. */ -public enum JSONKey { +public enum JSONKey: Sendable { case index(Int) case key(String) } diff --git a/Tests/SwiftJSONTests/ConcurrencyTests.swift b/Tests/SwiftJSONTests/ConcurrencyTests.swift new file mode 100644 index 00000000..bb51afde --- /dev/null +++ b/Tests/SwiftJSONTests/ConcurrencyTests.swift @@ -0,0 +1,386 @@ +// ConcurrencyTests.swift +// +// Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import XCTest +import SwiftyJSON + +class ConcurrencyTests: XCTestCase { + + // MARK: - Test 1: Actor Boundary Passing + + actor DataProcessor { + func process(_ json: JSON) -> String { + return json["name"].stringValue + } + + func processDictionary(_ json: JSON) -> Int { + return json["count"].intValue + } + + func processArray(_ json: JSON) -> Int { + return json[0].intValue + } + } + + func testActorBoundaryPassing() async { + let json = JSON(["name": "Test"]) + let processor = DataProcessor() + let result = await processor.process(json) + XCTAssertEqual(result, "Test") + } + + func testActorBoundaryPassingWithDictionary() async { + let json = JSON(["count": 42, "status": "active"]) + let processor = DataProcessor() + let result = await processor.processDictionary(json) + XCTAssertEqual(result, 42) + } + + func testActorBoundaryPassingWithArray() async { + let json = JSON([100, 200, 300]) + let processor = DataProcessor() + let result = await processor.processArray(json) + XCTAssertEqual(result, 100) + } + + // MARK: - Test 2: Sendable Closure and Task + + func testSendableClosureWithTask() async { + let json = JSON(["value": "Hello"]) + let task = Task { @Sendable () -> String in + return json["value"].stringValue + } + let result = await task.value + XCTAssertEqual(result, "Hello") + } + + func testSendableClosureWithComplexData() async { + let json = JSON([ + "user": ["name": "Alice", "age": 30], + "active": true + ]) + let task = Task { @Sendable () -> String in + return json["user"]["name"].stringValue + } + let result = await task.value + XCTAssertEqual(result, "Alice") + } + + // MARK: - Test 3: Async JSON Parsing + + func testAsyncJSONParsing() async throws { + let jsonString = "{\"status\":\"ok\"}" + let data = jsonString.data(using: .utf8)! + + let task = Task { + return try JSON(data: data) + } + + let json = try await task.value + let status = json["status"].stringValue + XCTAssertEqual(status, "ok") + } + + func testAsyncJSONParsingWithComplexData() async throws { + let jsonString = "{\"results\": [{\"id\": 1, \"name\": \"Item1\"}, {\"id\": 2, \"name\": \"Item2\"}]}" + let data = jsonString.data(using: .utf8)! + + let task = Task { + return try JSON(data: data) + } + + let json = try await task.value + XCTAssertEqual(json["results"].arrayValue.count, 2) + XCTAssertEqual(json["results"][0]["name"].stringValue, "Item1") + } + + // MARK: - Test 4: Concurrent Read Access with TaskGroup + + func testConcurrentReadAccess() async { + let json = JSON(["values": [1, 2, 3, 4, 5]]) + + await withTaskGroup(of: Int.self) { group in + for i in 0..<5 { + group.addTask { + return json["values"][i].intValue + } + } + + var sum = 0 + for await value in group { + sum += value + } + XCTAssertEqual(sum, 15) + } + } + + func testConcurrentReadAccessWithDictionary() async { + let json = JSON([ + "items": [ + ["id": 1, "value": 10], + ["id": 2, "value": 20], + ["id": 3, "value": 30] + ] + ]) + + await withTaskGroup(of: Int.self) { group in + for i in 0..<3 { + group.addTask { + return json["items"][i]["value"].intValue + } + } + + var sum = 0 + for await value in group { + sum += value + } + XCTAssertEqual(sum, 60) + } + } + + // MARK: - Test 5: MainActor Isolation + + @MainActor + func mainActorFunction(_ json: JSON) -> String { + return json["title"].stringValue + } + + func testMainActorIsolation() async { + let json = JSON(["title": "Main Thread Task"]) + let result = await mainActorFunction(json) + XCTAssertEqual(result, "Main Thread Task") + } + + // MARK: - Test 6: Task.detached Isolation + + func testTaskDetachedIsolation() async { + let json = JSON(["data": "detached"]) + + let task = Task.detached { @Sendable () -> String in + return json["data"].stringValue + } + + let result = await task.value + XCTAssertEqual(result, "detached") + } + + // MARK: - Test 7: Complex Nested JSON + + func testComplexNestedJSONConcurrency() async { + let json = JSON([ + "users": [ + [ + "id": 1, + "name": "Alice", + "posts": [ + ["id": 101, "title": "Post 1"], + ["id": 102, "title": "Post 2"] + ] + ], + [ + "id": 2, + "name": "Bob", + "posts": [ + ["id": 201, "title": "Post 3"] + ] + ] + ] + ]) + + await withTaskGroup(of: (Int, String).self) { group in + for i in 0..<2 { + group.addTask { + let userId = json["users"][i]["id"].intValue + let userName = json["users"][i]["name"].stringValue + return (userId, userName) + } + } + + var names: [String] = [] + for await (_, name) in group { + names.append(name) + } + + XCTAssert(names.contains("Alice")) + XCTAssert(names.contains("Bob")) + } + } + + func testComplexNestedJSONAccess() async { + let json = JSON([ + [ + ["deep": [1, 2, 3]], + ["deep": [4, 5, 6]] + ], + [ + ["deep": [7, 8, 9]], + ["deep": [10, 11, 12]] + ] + ]) + + let task = Task { @Sendable () -> Int in + return json[0][1]["deep"][2].intValue + } + + let result = await task.value + XCTAssertEqual(result, 6) + } + + // MARK: - Test 8: All JSON Types in Concurrent Context + + func testAllJSONTypesInConcurrency() async { + let json = JSON([ + "string": "value", + "number": 42, + "bool": true, + "null": NSNull(), + "array": [1, 2, 3], + "dictionary": ["nested": "object"] + ]) + + await withTaskGroup(of: String.self) { group in + group.addTask { + return json["string"].stringValue + } + group.addTask { + return String(json["number"].intValue) + } + group.addTask { + return String(json["bool"].boolValue) + } + group.addTask { + return json["null"].type == .null ? "null" : "not_null" + } + group.addTask { + return String(json["array"].arrayValue.count) + } + group.addTask { + return json["dictionary"]["nested"].stringValue + } + + var results: [String] = [] + for await result in group { + results.append(result) + } + + XCTAssertEqual(results.count, 6) + XCTAssert(results.contains("value")) + XCTAssert(results.contains("42")) + XCTAssert(results.contains("true")) + XCTAssert(results.contains("null")) + XCTAssert(results.contains("3")) + XCTAssert(results.contains("object")) + } + } + + // MARK: - Test 9: Error Handling Across Actor Boundaries + + actor ErrorProcessor { + func processJSON(_ json: JSON) throws -> String { + guard json["required"].exists() else { + throw SwiftyJSONError.notExist + } + return json["required"].stringValue + } + + func processWithTypeError(_ json: JSON) throws -> Int { + guard json["count"].type == .number else { + throw SwiftyJSONError.wrongType + } + return json["count"].intValue + } + } + + func testErrorHandlingAcrossActorBoundaries() async { + let json = JSON(["other": "value"]) + let processor = ErrorProcessor() + + do { + _ = try await processor.processJSON(json) + XCTFail("Should have thrown notExist error") + } catch SwiftyJSONError.notExist { + // Expected behavior + } catch { + XCTFail("Unexpected error type: \(error)") + } + } + + func testErrorHandlingWithWrongType() async { + let json = JSON(["count": "not_a_number"]) + let processor = ErrorProcessor() + + do { + _ = try await processor.processWithTypeError(json) + XCTFail("Should have thrown wrongType error") + } catch SwiftyJSONError.wrongType { + // Expected behavior + } catch { + XCTFail("Unexpected error type: \(error)") + } + } + + func testSuccessfulErrorHandling() async { + let json = JSON(["required": "present", "count": 123]) + let processor = ErrorProcessor() + + do { + let result = try await processor.processJSON(json) + XCTAssertEqual(result, "present") + + let count = try await processor.processWithTypeError(json) + XCTAssertEqual(count, 123) + } catch { + XCTFail("Should not have thrown error: \(error)") + } + } + + // MARK: - Test 10: Mixed Async Operations + + func testMixedAsyncOperations() async throws { + let jsonString = "{\"users\": [{\"id\": 1, \"name\": \"User1\"}, {\"id\": 2, \"name\": \"User2\"}]}" + let data = jsonString.data(using: .utf8)! + + let parseTask = Task { + return try JSON(data: data) + } + + let json = try await parseTask.value + + let names = await withTaskGroup(of: String.self) { group in + for i in 0..<2 { + group.addTask { + return json["users"][i]["name"].stringValue + } + } + + var result: [String] = [] + for await name in group { + result.append(name) + } + return result + } + + XCTAssertEqual(names.count, 2) + XCTAssert(names.contains("User1")) + XCTAssert(names.contains("User2")) + } +} From 636b70007f3ea3fe57df40151faaf1d62042fdaf Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Mon, 26 Jan 2026 17:27:31 -0700 Subject: [PATCH 16/19] Consolidate concurrency tests to 3 focused tests Each test now directly corresponds to a type made Sendable: - testJSONSendable: JSON crosses actor boundary - testTypeSendable: Type enum crosses actor boundary - testSwiftyJSONErrorSendable: SwiftyJSONError crosses actor boundary Removed redundant tests that proved the same thing in different ways (TaskGroup, Task.detached, MainActor variations). Added missing coverage for Type enum Sendable conformance. --- Tests/SwiftJSONTests/ConcurrencyTests.swift | 375 ++------------------ 1 file changed, 39 insertions(+), 336 deletions(-) diff --git a/Tests/SwiftJSONTests/ConcurrencyTests.swift b/Tests/SwiftJSONTests/ConcurrencyTests.swift index bb51afde..a6eaa099 100644 --- a/Tests/SwiftJSONTests/ConcurrencyTests.swift +++ b/Tests/SwiftJSONTests/ConcurrencyTests.swift @@ -23,364 +23,67 @@ import XCTest import SwiftyJSON +/// Tests verifying Sendable conformance for Swift 6 concurrency. +/// Each test targets a specific type made Sendable in this PR. class ConcurrencyTests: XCTestCase { - // MARK: - Test 1: Actor Boundary Passing - - actor DataProcessor { - func process(_ json: JSON) -> String { - return json["name"].stringValue - } - - func processDictionary(_ json: JSON) -> Int { - return json["count"].intValue - } - - func processArray(_ json: JSON) -> Int { - return json[0].intValue - } - } - - func testActorBoundaryPassing() async { - let json = JSON(["name": "Test"]) - let processor = DataProcessor() - let result = await processor.process(json) - XCTAssertEqual(result, "Test") - } - - func testActorBoundaryPassingWithDictionary() async { - let json = JSON(["count": 42, "status": "active"]) - let processor = DataProcessor() - let result = await processor.processDictionary(json) - XCTAssertEqual(result, 42) - } - - func testActorBoundaryPassingWithArray() async { - let json = JSON([100, 200, 300]) - let processor = DataProcessor() - let result = await processor.processArray(json) - XCTAssertEqual(result, 100) - } - - // MARK: - Test 2: Sendable Closure and Task - - func testSendableClosureWithTask() async { - let json = JSON(["value": "Hello"]) - let task = Task { @Sendable () -> String in - return json["value"].stringValue - } - let result = await task.value - XCTAssertEqual(result, "Hello") - } - - func testSendableClosureWithComplexData() async { - let json = JSON([ - "user": ["name": "Alice", "age": 30], - "active": true - ]) - let task = Task { @Sendable () -> String in - return json["user"]["name"].stringValue - } - let result = await task.value - XCTAssertEqual(result, "Alice") - } - - // MARK: - Test 3: Async JSON Parsing - - func testAsyncJSONParsing() async throws { - let jsonString = "{\"status\":\"ok\"}" - let data = jsonString.data(using: .utf8)! - - let task = Task { - return try JSON(data: data) + actor Processor { + func extract(_ json: JSON) -> String { + json["name"].stringValue } - let json = try await task.value - let status = json["status"].stringValue - XCTAssertEqual(status, "ok") - } - - func testAsyncJSONParsingWithComplexData() async throws { - let jsonString = "{\"results\": [{\"id\": 1, \"name\": \"Item1\"}, {\"id\": 2, \"name\": \"Item2\"}]}" - let data = jsonString.data(using: .utf8)! - - let task = Task { - return try JSON(data: data) + func extractType(_ json: JSON) -> Type { + json["value"].type } - let json = try await task.value - XCTAssertEqual(json["results"].arrayValue.count, 2) - XCTAssertEqual(json["results"][0]["name"].stringValue, "Item1") - } - - // MARK: - Test 4: Concurrent Read Access with TaskGroup - - func testConcurrentReadAccess() async { - let json = JSON(["values": [1, 2, 3, 4, 5]]) - - await withTaskGroup(of: Int.self) { group in - for i in 0..<5 { - group.addTask { - return json["values"][i].intValue - } - } - - var sum = 0 - for await value in group { - sum += value - } - XCTAssertEqual(sum, 15) - } - } - - func testConcurrentReadAccessWithDictionary() async { - let json = JSON([ - "items": [ - ["id": 1, "value": 10], - ["id": 2, "value": 20], - ["id": 3, "value": 30] - ] - ]) - - await withTaskGroup(of: Int.self) { group in - for i in 0..<3 { - group.addTask { - return json["items"][i]["value"].intValue - } - } - - var sum = 0 - for await value in group { - sum += value - } - XCTAssertEqual(sum, 60) - } - } - - // MARK: - Test 5: MainActor Isolation - - @MainActor - func mainActorFunction(_ json: JSON) -> String { - return json["title"].stringValue - } - - func testMainActorIsolation() async { - let json = JSON(["title": "Main Thread Task"]) - let result = await mainActorFunction(json) - XCTAssertEqual(result, "Main Thread Task") - } - - // MARK: - Test 6: Task.detached Isolation - - func testTaskDetachedIsolation() async { - let json = JSON(["data": "detached"]) - - let task = Task.detached { @Sendable () -> String in - return json["data"].stringValue - } - - let result = await task.value - XCTAssertEqual(result, "detached") - } - - // MARK: - Test 7: Complex Nested JSON - - func testComplexNestedJSONConcurrency() async { - let json = JSON([ - "users": [ - [ - "id": 1, - "name": "Alice", - "posts": [ - ["id": 101, "title": "Post 1"], - ["id": 102, "title": "Post 2"] - ] - ], - [ - "id": 2, - "name": "Bob", - "posts": [ - ["id": 201, "title": "Post 3"] - ] - ] - ] - ]) - - await withTaskGroup(of: (Int, String).self) { group in - for i in 0..<2 { - group.addTask { - let userId = json["users"][i]["id"].intValue - let userName = json["users"][i]["name"].stringValue - return (userId, userName) - } - } - - var names: [String] = [] - for await (_, name) in group { - names.append(name) - } - - XCTAssert(names.contains("Alice")) - XCTAssert(names.contains("Bob")) - } - } - - func testComplexNestedJSONAccess() async { - let json = JSON([ - [ - ["deep": [1, 2, 3]], - ["deep": [4, 5, 6]] - ], - [ - ["deep": [7, 8, 9]], - ["deep": [10, 11, 12]] - ] - ]) - - let task = Task { @Sendable () -> Int in - return json[0][1]["deep"][2].intValue - } - - let result = await task.value - XCTAssertEqual(result, 6) - } - - // MARK: - Test 8: All JSON Types in Concurrent Context - - func testAllJSONTypesInConcurrency() async { - let json = JSON([ - "string": "value", - "number": 42, - "bool": true, - "null": NSNull(), - "array": [1, 2, 3], - "dictionary": ["nested": "object"] - ]) - - await withTaskGroup(of: String.self) { group in - group.addTask { - return json["string"].stringValue - } - group.addTask { - return String(json["number"].intValue) - } - group.addTask { - return String(json["bool"].boolValue) - } - group.addTask { - return json["null"].type == .null ? "null" : "not_null" - } - group.addTask { - return String(json["array"].arrayValue.count) - } - group.addTask { - return json["dictionary"]["nested"].stringValue - } - - var results: [String] = [] - for await result in group { - results.append(result) - } - - XCTAssertEqual(results.count, 6) - XCTAssert(results.contains("value")) - XCTAssert(results.contains("42")) - XCTAssert(results.contains("true")) - XCTAssert(results.contains("null")) - XCTAssert(results.contains("3")) - XCTAssert(results.contains("object")) - } - } - - // MARK: - Test 9: Error Handling Across Actor Boundaries - - actor ErrorProcessor { - func processJSON(_ json: JSON) throws -> String { + func requireField(_ json: JSON) throws -> String { guard json["required"].exists() else { throw SwiftyJSONError.notExist } return json["required"].stringValue } - - func processWithTypeError(_ json: JSON) throws -> Int { - guard json["count"].type == .number else { - throw SwiftyJSONError.wrongType - } - return json["count"].intValue - } } - func testErrorHandlingAcrossActorBoundaries() async { - let json = JSON(["other": "value"]) - let processor = ErrorProcessor() - - do { - _ = try await processor.processJSON(json) - XCTFail("Should have thrown notExist error") - } catch SwiftyJSONError.notExist { - // Expected behavior - } catch { - XCTFail("Unexpected error type: \(error)") - } - } + /// Verifies JSON conforms to Sendable (can cross actor boundary) + func testJSONSendable() async { + let json = JSON(["name": "test", "count": 42]) + let processor = Processor() - func testErrorHandlingWithWrongType() async { - let json = JSON(["count": "not_a_number"]) - let processor = ErrorProcessor() + let result = await processor.extract(json) - do { - _ = try await processor.processWithTypeError(json) - XCTFail("Should have thrown wrongType error") - } catch SwiftyJSONError.wrongType { - // Expected behavior - } catch { - XCTFail("Unexpected error type: \(error)") - } + XCTAssertEqual(result, "test") } - func testSuccessfulErrorHandling() async { - let json = JSON(["required": "present", "count": 123]) - let processor = ErrorProcessor() + /// Verifies Type enum conforms to Sendable (can be returned across actor boundary) + func testTypeSendable() async { + let processor = Processor() - do { - let result = try await processor.processJSON(json) - XCTAssertEqual(result, "present") + let stringType = await processor.extractType(JSON(["value": "hello"])) + let numberType = await processor.extractType(JSON(["value": 123])) + let boolType = await processor.extractType(JSON(["value": true])) + let nullType = await processor.extractType(JSON(["value": NSNull()])) + let arrayType = await processor.extractType(JSON(["value": [1, 2, 3]])) + let dictType = await processor.extractType(JSON(["value": ["nested": "object"]])) - let count = try await processor.processWithTypeError(json) - XCTAssertEqual(count, 123) - } catch { - XCTFail("Should not have thrown error: \(error)") - } + XCTAssertEqual(stringType, .string) + XCTAssertEqual(numberType, .number) + XCTAssertEqual(boolType, .bool) + XCTAssertEqual(nullType, .null) + XCTAssertEqual(arrayType, .array) + XCTAssertEqual(dictType, .dictionary) } - // MARK: - Test 10: Mixed Async Operations + /// Verifies SwiftyJSONError conforms to Sendable (can be thrown across actor boundary) + func testSwiftyJSONErrorSendable() async { + let processor = Processor() - func testMixedAsyncOperations() async throws { - let jsonString = "{\"users\": [{\"id\": 1, \"name\": \"User1\"}, {\"id\": 2, \"name\": \"User2\"}]}" - let data = jsonString.data(using: .utf8)! - - let parseTask = Task { - return try JSON(data: data) - } - - let json = try await parseTask.value - - let names = await withTaskGroup(of: String.self) { group in - for i in 0..<2 { - group.addTask { - return json["users"][i]["name"].stringValue - } - } - - var result: [String] = [] - for await name in group { - result.append(name) - } - return result + do { + _ = try await processor.requireField(JSON(["other": "value"])) + XCTFail("Should have thrown") + } catch SwiftyJSONError.notExist { + // Error successfully crossed actor boundary + } catch { + XCTFail("Unexpected error: \(error)") } - - XCTAssertEqual(names.count, 2) - XCTAssert(names.contains("User1")) - XCTAssert(names.contains("User2")) } } From d24f061e37c6e0f40b99a53846e9d4682fe01d57 Mon Sep 17 00:00:00 2001 From: wongzigii Date: Wed, 4 Feb 2026 17:10:27 +0800 Subject: [PATCH 17/19] update ci --- .github/workflows/ci.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2680e910..a3816d45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,14 +12,12 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Set up Swift - uses: swift-actions/setup-swift@v2 - with: - swift-version: '6.0' + + - name: Select Xcode + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: Build - run: swift build --build-tests + run: xcrun swift build --build-tests - name: Run tests - run: swift test \ No newline at end of file + run: xcrun swift test \ No newline at end of file From 28094b0b74ea2c26003dee4d2b916a3943027ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jean=2E333=28=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=8B=E1=85=B2?= =?UTF-8?q?=E1=84=8C=E1=85=B5=E1=86=AB=29/kakaomobility?= <41438361+Yoojin99@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:14:18 +0900 Subject: [PATCH 18/19] Fix Tests group path in Xcode project --- SwiftyJSON.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftyJSON.xcodeproj/project.pbxproj b/SwiftyJSON.xcodeproj/project.pbxproj index ec223336..3c325ac2 100644 --- a/SwiftyJSON.xcodeproj/project.pbxproj +++ b/SwiftyJSON.xcodeproj/project.pbxproj @@ -278,7 +278,7 @@ 2E4FEFEB19575BE100351305 /* Supporting Files */, ); name = Tests; - path = Tests/Tes; + path = Tests/SwiftJSONTests; sourceTree = ""; }; 2E4FEFEB19575BE100351305 /* Supporting Files */ = { From d9712989ba2275f2d04e62a9fa74e704bc54fdda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jean=2E333=28=E1=84=8C=E1=85=A5=E1=86=BC=E1=84=8B=E1=85=B2?= =?UTF-8?q?=E1=84=8C=E1=85=B5=E1=86=AB=29/kakaomobility?= <41438361+Yoojin99@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:06:37 +0900 Subject: [PATCH 19/19] Fix test resource loading for Xcode and SwiftPM --- Tests/SwiftJSONTests/BaseTests.swift | 9 ++++++--- Tests/SwiftJSONTests/PerformanceTests.swift | 7 ++++++- Tests/SwiftJSONTests/SequenceTypeTests.swift | 7 ++++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Tests/SwiftJSONTests/BaseTests.swift b/Tests/SwiftJSONTests/BaseTests.swift index 634e1c8b..8bbebcde 100644 --- a/Tests/SwiftJSONTests/BaseTests.swift +++ b/Tests/SwiftJSONTests/BaseTests.swift @@ -31,9 +31,12 @@ class BaseTests: XCTestCase { super.setUp() -// let file = "./Tests/Tes/Tests.json" -// self.testData = try? Data(contentsOf: URL(fileURLWithPath: file)) - if let file = Bundle.module.url(forResource: "Tests", withExtension: "json") { + #if SWIFT_PACKAGE + let testBundle = Bundle.module + #else + let testBundle = Bundle(for: BaseTests.self) + #endif + if let file = testBundle.url(forResource: "Tests", withExtension: "json") { self.testData = try? Data(contentsOf: file) } else { XCTFail("Can't find the test JSON file") diff --git a/Tests/SwiftJSONTests/PerformanceTests.swift b/Tests/SwiftJSONTests/PerformanceTests.swift index 70f1f2f2..c0e6cc0b 100644 --- a/Tests/SwiftJSONTests/PerformanceTests.swift +++ b/Tests/SwiftJSONTests/PerformanceTests.swift @@ -30,7 +30,12 @@ class PerformanceTests: XCTestCase { override func setUp() { super.setUp() - if let file = Bundle.module.url(forResource: "Tests", withExtension: "json") { + #if SWIFT_PACKAGE + let testBundle = Bundle.module + #else + let testBundle = Bundle(for: PerformanceTests.self) + #endif + if let file = testBundle.url(forResource: "Tests", withExtension: "json") { self.testData = try? Data(contentsOf: file) } else { XCTFail("Can't find the test JSON file") diff --git a/Tests/SwiftJSONTests/SequenceTypeTests.swift b/Tests/SwiftJSONTests/SequenceTypeTests.swift index 557c3dbb..a039453f 100644 --- a/Tests/SwiftJSONTests/SequenceTypeTests.swift +++ b/Tests/SwiftJSONTests/SequenceTypeTests.swift @@ -28,7 +28,12 @@ class SequenceTypeTests: XCTestCase { var testData: Data? func testJSONFile() { - if let file = Bundle.module.url(forResource: "Tests", withExtension: "json") { + #if SWIFT_PACKAGE + let testBundle = Bundle.module + #else + let testBundle = Bundle(for: SequenceTypeTests.self) + #endif + if let file = testBundle.url(forResource: "Tests", withExtension: "json") { self.testData = try? Data(contentsOf: file) guard let json = try? JSON(data: self.testData!) else { XCTFail("Unable to parse the data")