Skip to content

Commit 5ce871d

Browse files
committed
Add extension to the JSONDecoder to allow to decode arrays and skip objects that failed to decode
1 parent 1ba20dc commit 5ce871d

5 files changed

Lines changed: 157 additions & 3 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// DecodingErrorIgnoringWrapper.swift
3+
//
4+
// Copyright © 2026 Aleksei Zaikin.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
//
24+
25+
import Foundation
26+
27+
struct DecodingErrorIgnoringWrapper<Wrapped: Decodable>: Decodable {
28+
let wrapped: Wrapped?
29+
30+
// MARK: - Init
31+
32+
init(from decoder: any Decoder) throws {
33+
do {
34+
self.wrapped = try Wrapped(from: decoder)
35+
} catch {
36+
NSLog("Couldn't decode '\(Wrapped.self)' from a JSON array: \(error). Skipping value.")
37+
self.wrapped = nil
38+
}
39+
}
40+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// JSONDecoder+IgnoringErrors.swift
3+
//
4+
// Copyright © 2026 Aleksei Zaikin.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
//
24+
25+
import Foundation
26+
27+
extension JSONDecoder {
28+
/// Returns an array of values of the type you specify, decoded from a JSON object.
29+
///
30+
/// If the data isn’t valid JSON, this method throws the DecodingError.dataCorrupted(_:) error.
31+
/// If a value within the JSON fails to decode, this method logs an error to the console, skips
32+
/// this value and continues to the next one.
33+
///
34+
/// - Parameters:
35+
/// - type: The type of the value to decode from the supplied JSON object.
36+
/// - data: The JSON object to decode.
37+
/// - Returns: An array of values of the specified type. This array may have no values if decoder
38+
/// couldn't decode anything from JSON array.
39+
/// - Throws: Decoding error if JSON data is corrupted.
40+
public func decodeArrayIgnoringErrors<Object: Decodable>(
41+
of type: Object.Type,
42+
from data: Data
43+
) throws -> [Object] {
44+
try decode([DecodingErrorIgnoringWrapper<Object>].self, from: data).compactMap(\.wrapped)
45+
}
46+
}

Sources/Instruments/Timer/Timer.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ public class Timer {
129129
return
130130
}
131131

132-
self.action()
133-
if !self.repeats {
134-
self.cancel()
132+
action()
133+
if !repeats {
134+
cancel()
135135
}
136136
}
137137
timer.schedule(
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//
2+
// JSONDecoder+IgnoringErrorsTests.swift
3+
//
4+
// Copyright © 2026 Aleksei Zaikin.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
//
24+
25+
import Foundation
26+
import Instruments
27+
import Testing
28+
29+
@Suite("JSON decoder + Ignoring errors")
30+
struct JSONDecoderIgnoringErrorsTests {
31+
private let decoder = JSONDecoder()
32+
33+
// MARK: - Tests
34+
35+
@Test("Returns complete array")
36+
func returnCompleteArray() throws {
37+
let jsonData = try #require("[1, 2, 3]".data(using: .utf8))
38+
39+
let entities = try decoder.decodeArrayIgnoringErrors(of: Int.self, from: jsonData)
40+
41+
#expect(entities.count == 3)
42+
#expect(entities[0] == 1)
43+
#expect(entities[1] == 2)
44+
#expect(entities[2] == 3)
45+
}
46+
47+
@Test("Return array skipping object if couldn't decode one")
48+
func returnBySkipping() throws {
49+
let jsonData = try #require("[1, \"2\", 3]".data(using: .utf8))
50+
51+
let entities = try decoder.decodeArrayIgnoringErrors(of: Int.self, from: jsonData)
52+
53+
#expect(entities.count == 2)
54+
#expect(entities[0] == 1)
55+
#expect(entities[1] == 3)
56+
}
57+
58+
@Test("Throws error if data corrupted")
59+
func returnEmptyArray() throws {
60+
let jsonData = try #require("[1, 2 3]".data(using: .utf8))
61+
62+
#expect(throws: DecodingError.self) {
63+
try decoder.decodeArrayIgnoringErrors(of: Int.self, from: jsonData)
64+
}
65+
}
66+
}

Tests/InstrumentsTests/Macros/EnumMacroTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ import SwiftSyntaxMacros
2929
import SwiftSyntaxMacrosTestSupport
3030
import Testing
3131

32+
@MainActor
3233
let testMacros: [String: any Macro.Type] = [
3334
"Enum": EnumMacro.self
3435
]
3536

3637
// MARK: -
3738

39+
@MainActor
3840
@Suite("Enum macro")
3941
struct EnumMacroTests {
4042
@Test("Is expanded successfully")

0 commit comments

Comments
 (0)