Skip to content

Update JSON usage from Any to JSONValue#740

Open
stevenzeck wants to merge 14 commits intoreadium:developfrom
stevenzeck:feature/json-value-migration
Open

Update JSON usage from Any to JSONValue#740
stevenzeck wants to merge 14 commits intoreadium:developfrom
stevenzeck:feature/json-value-migration

Conversation

@stevenzeck
Copy link
Contributor

This PR does the following:

  1. Moves JSON.swift out of ReadiumInternal and into ReadiumShared. It was also renamed to JSONDictionary to avoid conflicts with the existing JSON.swift in ReadiumShared.
  2. All uses of Any or [String: Any] were updated to use JSONValue.
  3. init(json: [String: Any]?) and the like were left for convenience, but ultimately use the new init in it.

Note this has breaking changes. I've drafted them in the Migration Guide.

Renamed to avoid conflicts with the existing JSON.swift file in Shared
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates JSON handling across the Readium Swift codebase from untyped Any / [String: Any] to the type-safe JSONValue, and relocates/renames the JSONDictionary helper into ReadiumShared to support Equatable/Hashable JSON-backed models.

Changes:

  • Introduces JSONDictionary (now backed by [String: JSONValue]) plus updated parsing/encoding helpers, and removes the old internal JSON.swift.
  • Updates core publication models, parsers, and format sniffers to parse/serialize via JSONValue (with compatibility init(json: Any?) kept in many places).
  • Updates test fixtures and assertion helpers to work with JSONValue-based JSON structures and normalization.

Reviewed changes

Copilot reviewed 75 out of 75 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Tests/StreamerTests/Parser/Image/ComicInfoParserTests.swift Adjusts test expectations for otherMetadata to use JSONValue accessors.
Tests/StreamerTests/Parser/EPUB/Services/EPUBPositionsServiceTests.swift Updates test helper properties to build JSONValue objects/arrays.
Tests/StreamerTests/Parser/EPUB/EPUBMetadataParserTests.swift Rewrites metadata fixtures to use .object/.array/.string JSONValue literals.
Tests/StreamerTests/Parser/EPUB/EPUBManifestParserTests.swift Rewrites manifest fixtures to use JSONValue.
Tests/StreamerTests/Asserts.swift Normalizes Any via JSONValue(...).any before JSONSerialization compare.
Tests/SharedTests/Publication/Services/Content/Iterators/HTMLResourceContentIteratorTests.swift Updates locator otherLocations test data to .string.
Tests/SharedTests/Publication/LocatorTests.swift Updates minimal JSON expectation to align with new encoding rules.
Tests/SharedTests/Publication/Extensions/OPDS/Properties+OPDSTests.swift Removes legacy [String: Any] cast in test fixture.
Tests/SharedTests/Publication/Extensions/HTML/Locator+HTMLTests.swift Removes legacy [String: Any] cast in test fixture.
Tests/SharedTests/Publication/Extensions/EPUB/Metadata+EPUBTests.swift Updates EPUBMediaOverlay JSON fixtures to new typed JSON forms.
Tests/SharedTests/Publication/Extensions/Archive/Properties+ArchiveTests.swift Removes legacy [String: Any] cast in test fixture.
Tests/SharedTests/Asserts.swift Updates file default and normalizes Any via JSONValue before compare.
Support/Carthage/Readium.xcodeproj/project.pbxproj Moves JSONDictionary.swift into Shared target sources; removes Internal JSON.swift.
Support/Carthage/.xcodegen Updates XcodeGen inputs to reflect JSONDictionary move/rename.
Sources/Streamer/Parser/Readium/ReadiumWebPubParser.swift Switches manifest decoding to asJSONObjectValue() and wraps into .object.
Sources/Streamer/Parser/Readium/ReadiumGuidedNavigationService.swift Switches JSON decoding to asJSONObjectValue() and wraps into .object.
Sources/Streamer/Parser/Image/ComicInfoParser.swift Encodes ComicInfo otherMetadata as [String: JSONValue] strings.
Sources/Streamer/Parser/EPUB/OPFParser.swift Encodes properties/encryption using JSONValue objects/arrays.
Sources/Streamer/Parser/EPUB/OPFMeta.swift Converts unknown metadata aggregation to [String: JSONValue].
Sources/Streamer/Parser/EPUB/EPUBMetadataParser.swift Wraps mediaOverlay JSON into .object in otherMetadata.
Sources/Shared/Toolkit/JSONDictionary.swift Adds new Shared JSONDictionary + parse/encode helper APIs using JSONValue.
Sources/Shared/Toolkit/JSON.swift Updates serialization + JSONEquatable to better interop with JSONValue.
Sources/Shared/Toolkit/Format/Sniffers/RWPMFormatSniffer.swift Uses asJSONObjectValue() and wraps into .object for Manifest init.
Sources/Shared/Toolkit/Format/Sniffers/RPFFormatSniffer.swift Uses asJSONObjectValue() and wraps into .object for Manifest init.
Sources/Shared/Toolkit/Format/Sniffers/OPDSFormatSniffer.swift Uses asJSONObjectValue() and wraps into .object for Manifest init.
Sources/Shared/Toolkit/Format/Sniffers/LCPLicenseFormatSniffer.swift Adjusts JSON reading path and guards with updated optional handling.
Sources/Shared/Toolkit/Format/Sniffers/JSONFormatSniffer.swift Updates to reflect readAsJSON() now yielding JSONValue?.
Sources/Shared/Toolkit/Format/FormatSnifferBlob.swift Changes cached JSON from Any? to JSONValue?.
Sources/Shared/Toolkit/Archive/ArchiveProperties.swift Migrates ArchiveProperties JSON parsing/encoding to JSONValue.
Sources/Shared/Publication/TDM.swift Migrates TDM JSON parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Subject.swift Migrates Subject parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Services/Positions/PositionsService.swift Emits/consumes positions JSON using JSONValue and asJSONObjectValue().
Sources/Shared/Publication/Services/Content/Iterators/HTMLResourceContentIterator.swift Makes locator otherLocations cssSelector explicitly typed JSONValue.
Sources/Shared/Publication/PublicationCollection.swift Migrates collection JSON parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Publication.swift Adds init(json: JSONValue) and bridges Any to JSONValue.
Sources/Shared/Publication/Properties.swift Migrates properties storage and subscript to JSONValue.
Sources/Shared/Publication/Metadata.swift Migrates metadata parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Manifest.swift Migrates manifest parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Locator.swift Migrates locator + nested types parsing/encoding to JSONValue with Any convenience inits.
Sources/Shared/Publication/LocalizedString.swift Migrates LocalizedString parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/LinkRelation.swift Migrates LinkRelation array parsing to JSONValue with Any convenience init.
Sources/Shared/Publication/Link.swift Migrates Link parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/GuidedNavigation/GuidedNavigationObject.swift Migrates guided navigation object parsing to JSONValue with Any convenience init.
Sources/Shared/Publication/GuidedNavigation/GuidedNavigationDocument.swift Migrates guided navigation document parsing to JSONValue with Any convenience init.
Sources/Shared/Publication/Extensions/Presentation/Properties+Presentation.swift Updates property access to JSONValue.bool.
Sources/Shared/Publication/Extensions/Presentation/Presentation.swift Migrates Presentation parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Extensions/HTML/Locator+HTML.swift Updates otherLocations access to JSONValue.string.
Sources/Shared/Publication/Extensions/HTML/DOMRange.swift Migrates DOMRange parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Extensions/Encryption/Encryption.swift Migrates Encryption parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Extensions/EPUB/EPUBMediaOverlay.swift Migrates EPUBMediaOverlay parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Extensions/Archive/Properties+Archive.swift Migrates archive properties parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Contributor.swift Migrates Contributor parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/Publication/Accessibility/Accessibility.swift Migrates Accessibility parsing/encoding to JSONValue with Any convenience init.
Sources/Shared/OPDS/OPDSPrice.swift Migrates OPDSPrice parsing/encoding to JSONValue.
Sources/Shared/OPDS/OPDSHolds.swift Migrates OPDSHolds parsing to JSONValue-backed JSONDictionary.
Sources/Shared/OPDS/OPDSCopies.swift Migrates OPDSCopies parsing to JSONValue-backed JSONDictionary.
Sources/Shared/OPDS/OPDSAvailability.swift Migrates OPDSAvailability parsing to JSONValue-backed JSONDictionary.
Sources/Shared/OPDS/OPDSAcquisition.swift Migrates OPDSAcquisition encoding to JSONValue and adjusts parsing.
Sources/OPDS/OPDS1Parser.swift Emits OPDS link properties using [String: JSONValue].
Sources/Navigator/EPUB/HTMLDecorationTemplate.swift Migrates decoration template JSON output to [String: JSONValue].
Sources/LCP/License/Model/StatusDocument.swift Migrates LSD parsing to JSONValue-backed JSONDictionary and helpers.
Sources/LCP/License/Model/LicenseDocument.swift Migrates license document parsing to JSONValue-backed JSONDictionary and helpers.
Sources/LCP/License/Model/Components/Links.swift Adds JSONValue-based Links initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/Link.swift Adds JSONValue-based Link initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/LSD/PotentialRights.swift Adds JSONValue-based initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/LSD/Event.swift Adds JSONValue-based initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/LCP/UserKey.swift Adds JSONValue-based initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/LCP/User.swift Migrates extensions storage to [String: JSONValue] and JSONValue parsing.
Sources/LCP/License/Model/Components/LCP/Signature.swift Adds JSONValue-based initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/LCP/Rights.swift Migrates extensions storage to [String: JSONValue] and JSONValue parsing.
Sources/LCP/License/Model/Components/LCP/Encryption.swift Adds JSONValue-based initializer; bridges legacy initializer.
Sources/LCP/License/Model/Components/LCP/ContentKey.swift Adds JSONValue-based initializer; bridges legacy initializer.
Sources/Internal/JSON.swift Removes the old Internal JSONDictionary/parse helpers implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 266 to 272
if
let mediaOverlayId = item.mediaOverlayId,
let smilIndex = items.firstIndex(where: { $0.id == mediaOverlayId && $0.link.mediaType?.matches(.smil) == true })
let mediaOverlayIndex = items.firstIndex(where: { $0.id == mediaOverlayId })
{
let smilItem = items.remove(at: smilIndex)
spineLink.alternates.append(smilItem.link)
let mediaOverlayItem = items.remove(at: mediaOverlayIndex)
spineLink.alternates.append(mediaOverlayItem.link)
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Media overlays are now attached based only on id match, without verifying the target item is actually a SMIL resource. If the OPF is malformed (or IDs collide), this can remove an arbitrary manifest item from resources and add a non-SMIL alternate. Restoring the media type check (or otherwise validating the overlay target) would prevent unintended removals.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +46
var json: [String: JSONValue] {
[
"entryLength": entryLength as NSNumber,
"isEntryCompressed": isEntryCompressed,
"entryLength": .integer(Int(entryLength)),
"isEntryCompressed": .bool(isEntryCompressed),
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Encoding entryLength as .integer(Int(entryLength)) can trap if entryLength exceeds Int.max. To avoid a potential crash on large entries, encode without lossy/unsafe narrowing (e.g. clamp, encode as .double, or encode as a string) and parse accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 22
public func serializeJSONString(_ object: Any) -> String? {
let unwrappedObject = JSONValue(object)?.any ?? object

guard
let data = try? JSONSerialization.data(withJSONObject: object, options: .sortedKeys),
JSONSerialization.isValidJSONObject(unwrappedObject) || JSONSerialization.isValidJSONObject([unwrappedObject]),
let data = try? JSONSerialization.data(withJSONObject: unwrappedObject, options: .sortedKeys),
let string = String(data: data, encoding: .utf8)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

serializeJSONString allows JSON fragments via isValidJSONObject([unwrappedObject]), but still calls JSONSerialization.data(withJSONObject: unwrappedObject), which will always fail for fragment-only values (e.g. String/Int/Bool). Either remove the fragment check, or actually serialize as a fragment (e.g. using .fragmentsAllowed or wrapping in an array and unwrapping consistently).

Copilot uses AI. Check for mistakes.
Comment on lines 48 to 66
init(json: Any?, warnings: WarningLogger? = nil) {
self.init()
guard let json = json as? [[String: Any]] else {
guard let json = json else {
return
}

let acquisitions = json.compactMap { try? OPDSAcquisition(json: $0, warnings: warnings) }
let rawJson: Any
if let j = json as? JSONValue {
rawJson = j.any
} else {
rawJson = json
}

guard let array = rawJson as? [[String: Any]] else {
return
}

let acquisitions = array.compactMap { try? OPDSAcquisition(json: $0, warnings: warnings) }
append(contentsOf: acquisitions)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array<OPDSAcquisition>.init(json:) casts to [[String: Any]], which won’t work when the JSON comes from the new json properties ([[String: JSONValue]]). This breaks round-tripping and parsing of already-normalized JSONValue trees. Consider switching this initializer to accept JSONValue? and building acquisitions with parseArray(json) (mapping each element into OPDSAcquisition).

Copilot uses AI. Check for mistakes.
@stevenzeck
Copy link
Contributor Author

@mickael-menu I'll leave the remaining Copilot comments as unresolved in case you want to review the changes I made based on them.

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