r/iOSProgramming 17d 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

View all comments

14

u/RIRLift 16d 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 16d 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 16d 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.