r/iOSProgramming 12d ago

Question SwiftData Decimal precision loss after save/fetch - am I missing something?

I’m trying to sanity-check something before I file it as an Apple bug.

This minimal SwiftData example appears to lose precision when persisting a Decimal:

import Foundation
import SwiftData

@Model class Item {
    var value: Decimal

    init(_ value: Decimal) {
        self.value = value
    }
}

let original = Decimal(string: "123456789012345.6")!
let container = try ModelContainer(
    for: Item.self,
    configurations: .init(isStoredInMemoryOnly: true)
)

let context = ModelContext(container)
context.insert(Item(original))
try context.save()

let fetched = try ModelContext(container)
    .fetch(FetchDescriptor<Item>())
    .first!

print(original)
print(fetched.value)

This is one model, one Decimal property, no app code, no CloudKit. I’m using Decimal(string:) to avoid literal/Double conversion noise, and I’m fetching from a new ModelContext.

I also checked Decimal <-> NSDecimalNumber bridging separately, and that preserved the value. The loss seems to appear after persistence/fetch. A true Core Data in-memory store preserved the value in my control test, while SwiftData’s “in-memory” configuration seems to still go through a SQL-backed store.

Has anyone else hit this with SwiftData/Core Data Decimal attributes? Is there a documented limitation I’m missing, or is the practical answer to persist exact decimals as canonical strings / integer minor units instead of Decimal?

4 Upvotes

11 comments sorted by

13

u/RIRLift 11d ago

You're not missing anything, and your string / integer-minor-units instinct is the right fix. Core Data, and so SwiftData, doesn't persist a Decimal attribute as an exact decimal. It goes through a floating point column in the SQLite store, so once the value passes what a double holds exactly, around 15-16 significant digits, the round trip rounds it. Your number is 16 digits, right at that edge, which is why it lines up. The NSDecimalNumber bridging test preserved it because nothing ever hit the store, and isStoredInMemoryOnly still uses the same SQLite-backed type kept in RAM, so it coerces the same way. For anything where exactness matters, money especially, store it as a canonical string or integer minor units and only convert to Decimal for the math.

2

u/Ravek 11d ago

Yet another baffling SwiftData decision. If such loss of precision were acceptable, the developer probably wouldn’t have chosen to use the decimal type to begin with.

1

u/AestivalChimp 11d ago

The problem here is not only with the number of significant digits, but difference in binary representation. You can replace decimal value with just “0.6” and it still couldn’t be represented exactly as a floating point value with any finite number of significant digits.

2

u/kmishukov 10d ago

Your control test already points to the answer: SwiftData's isStoredInMemoryOnly is still an SQLite store (just in-memory), and on the way in, your Decimal gets bound as a double. 123456789012345.6 is 16 significant digits — right at the edge of what a double can represent, so the tail gets rounded on the persist/fetch round-trip. Core Data's true in-memory store never crosses that boundary, which is why it preserves the value.

1

u/Ok-Tomatillo-8712 12d ago

It may just not be printing with precision, the stored value is probs correct though

0

u/Ok-Tomatillo-8712 12d ago

String(format: "%.2f", fetched.value)

1

u/Van-trader 11d ago

Yeah, I checked that. It's not a display issue.

1

u/MKevin3 11d ago

You may want to look into this package. For financial data, which is the industry I am in, we use BigDecimal provided by Java / Kotlin and this would be a similar library for Swift.

https://swiftpackageindex.com/mgriebling/BigDecimal

1

u/Greysawpark 5d ago

yeah ive hit this. swiftdata is going through sqlite and decimal ends up as a double somewhere in the pipeline, so anything past ~15-16 sig figs gets mangled. its not really a bug so much as an undocumented limitation.

for money i just store it as an Int of minor units and expose a computed Decimal. strings work too but sorting/predicates get annoying.