Skip to content

Fix deserialization Data objects that contain structured data (array/dict)#7

Open
fabiosoft wants to merge 1 commit into
ThatsJustCheesy:masterfrom
fabiosoft:fix-array-deserialization
Open

Fix deserialization Data objects that contain structured data (array/dict)#7
fabiosoft wants to merge 1 commit into
ThatsJustCheesy:masterfrom
fabiosoft:fix-array-deserialization

Conversation

@fabiosoft
Copy link
Copy Markdown
Contributor

Summary

Fixed an issue where array values stored in macOS defaults were displayed as empty when attempting to edit them, even though they contained data.

Problem

When opening certain preference domains, some array fields would appear as "New empty array" in the editor, despite showing a size (e.g., "235 bytes"). This made it impossible to view or modify the actual array contents through the UI.

Solution

Improved the way preference data is loaded and parsed to properly handle arrays that are stored in serialized formats. The editor now correctly recognizes and displays these values as editable arrays with their actual content.

Testing

Tested with domains containing serialized array data. Arrays now display their content correctly and can be edited as expected.

@ThatsJustCheesy
Copy link
Copy Markdown
Owner

ThatsJustCheesy commented Jan 18, 2026

Hey again @fabiosoft, I hope to get around to thoroughly reviewing this. In the meantime, could you take a look at the following things?

  • I see a bunch of synchronize / CFPreferencesAppSynchronize calls were added. From your testing, are those necessary for the app to work? Otherwise I'd be inclined to remove them.
  • I've tried to avoid resorting to the defaults command for write operations, not least because it handles edits to nested attributes pretty poorly – if there's, say, a dict-with-nested-array structure, then to edit some item in the array, you have to pass the defaults command -dict and a serialized version of the entire dict, including the entire array. At that point, you'd might as well use UserDefaults or the equivalent CF APIs directly. (Unless those don't handle this kind of <data>/NSData deserialization correctly?)

thanks!

@fabiosoft
Copy link
Copy Markdown
Contributor Author

Hello!

  • synchronize / CFPreferencesAppSynchronize: I added these to avoid “stale” reads after write/delete and to force the flush of the cfprefsd cache, since in some places the UI reloads immediately via defaults export or dictionaryRepresentation(). I don't have strong evidence that they are indispensable in all cases: if you prefer, I can remove them and check if the app continues to behave correctly without them.

  • defaults command for write: I only use it for scalar types (string/bool/int/float/date/data). For array/dictionary, I continue to use UserDefaults precisely to avoid the problem you mention with nested structures. If you prefer to completely eliminate defaults and only use CFPreferences/UserDefaults for each write, I can refactor in that direction.

I looked into it further and modified these lines of code because it wasn't accepting my changes and saving them when I added or removed elements. Now it works for me.

Let me know which way you prefer, so I can align the patch.

reference:

struct UserDefaultsDomain: DefaultsModifier {
    func add(_ item: PlistItem) {
        let (key, value) = item.persistentRepresentation

        // Write using defaults command for immediate disk persistence
        writeValue(value, forKey: key)
        synchronize()
    }

    func removeItems(for keys: Set<String>) {
        for key in keys {
            deleteKey(key)
        }
        synchronize()
    }

    func synchronize() {
        // Flush cfprefsd cache to disk
        let appID = domainName as CFString
        CFPreferencesAppSynchronize(appID)
    }
    // ...
    private func writeValue(_ value: Any?, forKey key: String) {
        // defaults command only for scalar types
        // ...
        case let dict as [String: Any]:
            userDefaults.set(dict, forKey: key)
            let _: Bool = userDefaults.synchronize()
            return
        case let array as [Any]:
            userDefaults.set(array, forKey: key)
            let _: Bool = userDefaults.synchronize()
            return
        // ...
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants