From 68397fc9d885168e410959ff557545df119dacb5 Mon Sep 17 00:00:00 2001 From: harshitha-cstk Date: Mon, 18 May 2026 16:06:34 +0530 Subject: [PATCH 1/3] enh: add back-merge workflow from master to development and remove obsolete check-branch workflow --- .github/workflows/back-merge-pr.yml | 54 +++++++++++++++++++++++++++++ .github/workflows/check-branch.yml | 20 ----------- 2 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/back-merge-pr.yml delete mode 100644 .github/workflows/check-branch.yml diff --git a/.github/workflows/back-merge-pr.yml b/.github/workflows/back-merge-pr.yml new file mode 100644 index 00000000..02b378ce --- /dev/null +++ b/.github/workflows/back-merge-pr.yml @@ -0,0 +1,54 @@ +name: Back-merge master to development + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + open-back-merge-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Open back-merge PR if needed + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + BASE_BRANCH="development" + SOURCE_BRANCH="master" + + git fetch origin "$BASE_BRANCH" "$SOURCE_BRANCH" + + if ! git show-ref --verify --quiet "refs/remotes/origin/$BASE_BRANCH"; then + echo "Base branch '$BASE_BRANCH' does not exist on origin; skipping." + exit 0 + fi + + SOURCE_SHA=$(git rev-parse "origin/$SOURCE_BRANCH") + BASE_SHA=$(git rev-parse "origin/$BASE_BRANCH") + + if [ "$SOURCE_SHA" = "$BASE_SHA" ]; then + echo "$SOURCE_BRANCH and $BASE_BRANCH are at the same commit; nothing to back-merge." + exit 0 + fi + + EXISTING=$(gh pr list --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --state open --json number --jq 'length') + + if [ "$EXISTING" -gt 0 ]; then + echo "An open PR from $SOURCE_BRANCH to $BASE_BRANCH already exists; skipping." + exit 0 + fi + + gh pr create --repo "${{ github.repository }}" --base "$BASE_BRANCH" --head "$SOURCE_BRANCH" --title "chore: back-merge $SOURCE_BRANCH into $BASE_BRANCH" --body "Automated back-merge after changes landed on \\`$SOURCE_BRANCH\\`. Review and merge to keep \\`$BASE_BRANCH\\` in sync." + + echo "Created back-merge PR $SOURCE_BRANCH -> $BASE_BRANCH." diff --git a/.github/workflows/check-branch.yml b/.github/workflows/check-branch.yml deleted file mode 100644 index 8a3a32ab..00000000 --- a/.github/workflows/check-branch.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'Check Branch' - -on: - pull_request: - -jobs: - check_branch: - runs-on: ubuntu-latest - steps: - - name: Comment PR - if: github.base_ref == 'master' && github.head_ref != 'staging' - uses: thollander/actions-comment-pull-request@v2 - with: - message: | - We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the staging branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch. - - name: Check branch - if: github.base_ref == 'master' && github.head_ref != 'staging' - run: | - echo "ERROR: We regret to inform you that you are currently not able to merge your changes into the master branch due to restrictions applied by our SRE team. To proceed with merging your changes, we kindly request that you create a pull request from the staging branch. Our team will then review the changes and work with you to ensure a successful merge into the master branch." - exit 1 \ No newline at end of file From 7ba850736cd5c08ac4dc392707c0cb7f6a0994f7 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Fri, 12 Jun 2026 12:38:28 +0530 Subject: [PATCH 2/3] feat: implement toJSON() methods for EntryModel , AssetModel and ContentTypeModel to enable JSON serialization --- CHANGELOG.md | 8 + .../ContentstackSwift iOS Tests.xcscheme | 22 +- Sources/AssetModel.swift | 7 + Sources/ContentTypeModel.swift | 22 ++ Sources/EntryModel.swift | 29 ++ Tests/DecodableTest.swift | 274 +++++++++++++++++- 6 files changed, 360 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cc00e04..fc6835ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v2.3.4 + +### Date: 12-Jun-2026 + +### Bug Fixes + +- Added `toJSON()` to `EntryModel`, `AssetModel`, and `ContentTypeModel` to make entries with resolved references serializable via `JSONSerialization`. + ## v2.3.3 ### Date: 18-May-2026 diff --git a/ContentstackSwift.xcodeproj/xcshareddata/xcschemes/ContentstackSwift iOS Tests.xcscheme b/ContentstackSwift.xcodeproj/xcshareddata/xcschemes/ContentstackSwift iOS Tests.xcscheme index 3fd1a138..eea27f4d 100644 --- a/ContentstackSwift.xcodeproj/xcshareddata/xcschemes/ContentstackSwift iOS Tests.xcscheme +++ b/ContentstackSwift.xcodeproj/xcshareddata/xcschemes/ContentstackSwift iOS Tests.xcscheme @@ -1,11 +1,22 @@ + version = "2.2"> + + + + + + + + + + diff --git a/Sources/AssetModel.swift b/Sources/AssetModel.swift index b3aee035..b66b6024 100644 --- a/Sources/AssetModel.swift +++ b/Sources/AssetModel.swift @@ -84,6 +84,13 @@ public final class AssetModel: AssetDecodable { fields = try containerFields.decode(Dictionary.self) } + /// Returns a JSON-serializable `[String: Any]` dictionary suitable for use with + /// `JSONSerialization`. Recursively converts any nested SDK model objects to plain + /// dictionaries. See `EntryModel.toJSON()` for context on why this is needed. + public func toJSON() -> [String: Any] { + return EntryModel.normalizeForJSON(fields ?? [:]) as? [String: Any] ?? [:] + } + public enum QueryableCodingKey: String, CodingKey { case uid, title case fileName = "filename" diff --git a/Sources/ContentTypeModel.swift b/Sources/ContentTypeModel.swift index c00cc13f..ffa72b46 100644 --- a/Sources/ContentTypeModel.swift +++ b/Sources/ContentTypeModel.swift @@ -35,6 +35,28 @@ public final class ContentTypeModel: SystemFields, Decodable { } } + /// Returns a JSON-serializable `[String: Any]` dictionary suitable for use with + /// `JSONSerialization`. See `EntryModel.toJSON()` for context on why this is needed. + public func toJSON() -> [String: Any] { + var json: [String: Any] = [ + "uid": uid, + "title": title, + // schema entries originate from the decoder and are JSON-native, but normalize + // defensively in case any nested model object is present. + "schema": EntryModel.normalizeForJSON(schema) + ] + if let description = description { json["description"] = description } + if let createdAt = createdAt { json["created_at"] = formatDate(createdAt) } + if let updatedAt = updatedAt { json["updated_at"] = formatDate(updatedAt) } + return json + } + + private func formatDate(_ date: Date) -> String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: date) + } + public enum FieldKeys: String, CodingKey { case title, uid, description case createdAt = "created_at" diff --git a/Sources/EntryModel.swift b/Sources/EntryModel.swift index a7f4bf43..b50a13a5 100644 --- a/Sources/EntryModel.swift +++ b/Sources/EntryModel.swift @@ -58,4 +58,33 @@ public class EntryModel: EntryDecodable, ContentTypeIncludable { let containerFields = try decoder.container(keyedBy: JSONCodingKeys.self) fields = try containerFields.decode(Dictionary.self) } + + /// Returns a JSON-serializable `[String: Any]` dictionary suitable for use with + /// `JSONSerialization`. When `include_all` or reference includes are used, the SDK + /// stores nested `EntryModel`/`AssetModel`/`ContentTypeModel` objects inside the + /// `fields` dictionary. Those Swift objects are not accepted by `JSONSerialization`, + /// so this method recursively converts them to plain dictionaries before returning. + public func toJSON() -> [String: Any] { + return EntryModel.normalizeForJSON(fields ?? [:]) as? [String: Any] ?? [:] + } + + static func normalizeForJSON(_ value: Any) -> Any { + switch value { + case let entry as EntryModel: + return entry.toJSON() + case let asset as AssetModel: + return asset.toJSON() + case let contentType as ContentTypeModel: + return contentType.toJSON() + case let array as [Any]: + return array.map { normalizeForJSON($0) } + case let dict as [String: Any]: + return dict.mapValues { normalizeForJSON($0) } + default: + // Reached for JSON-native scalars (String/NSNumber/Bool/NSNull). The SDK decoder + // only ever stores these or the three model types above inside `fields`. If a new + // decodable model type is ever added to Decodable.swift, add a case for it here. + return value + } + } } diff --git a/Tests/DecodableTest.swift b/Tests/DecodableTest.swift index eb4b7487..0e0369aa 100644 --- a/Tests/DecodableTest.swift +++ b/Tests/DecodableTest.swift @@ -420,6 +420,272 @@ class DecodableTest: XCTestCase { } } + // MARK: - EntryModel.toJSON() Tests + + // Decodes an entry's fields into a Codable model via JSONSerialization + JSONDecoder. + private static func parseModel(_ model: [String: Any]) -> T? { + do { + let data = try JSONSerialization.data(withJSONObject: model) + return try JSONDecoder().decode(T.self, from: data) + } catch { + return nil + } + } + + private struct ParsedEntry: Decodable { + let uid: String + let title: String + let author_group: ParsedReferenceGroup + } + private struct ParsedReferenceGroup: Decodable { + let show_author: Bool + let authors: [ParsedReferencedEntry] + } + private struct ParsedReferencedEntry: Decodable { + let uid: String + let title: String + } + + /// A reference field resolved (via include_all) to a full entry is decoded as an `EntryModel` + /// inside `fields`, which is not JSON-native. `toJSON()` flattens it so it can be serialized. + func testEntryModel_includeAll_referenceFieldIsEntry_parsesViaToJSON() throws { + let apiResponseJSON = """ + { + "uid": "mock_parent_uid", + "title": "Blog Post", + "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", + "updated_by": "mock_user", + "author_group": { + "show_author": true, + "authors": [ + { + "uid": "mock_referenced_uid", + "title": "Referenced Author Entry", + "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", + "updated_by": "mock_user" + } + ] + } + } + """ + + let data = apiResponseJSON.data(using: .utf8)! + let entry = try JSONDecoder.dateDecodingStrategy().decode(EntryModel.self, from: data) + + // Reference field is resolved into an EntryModel inside fields. + let group = entry.fields?["author_group"] as? [String: Any] + let refArray = group?["authors"] as? [Any] + XCTAssertTrue(refArray?.first is EntryModel) + + // Raw fields contain a Swift object, so they are not a valid JSON object. + // (Avoid JSONSerialization.data on raw fields — it raises an uncatchable NSException.) + XCTAssertFalse(JSONSerialization.isValidJSONObject(entry.fields ?? [:])) + + // toJSON() yields a valid JSON object that round-trips into a Codable struct. + let normalized = entry.toJSON() + XCTAssertTrue(JSONSerialization.isValidJSONObject(normalized)) + + let parsed: ParsedEntry? = DecodableTest.parseModel(normalized) + XCTAssertNotNil(parsed) + XCTAssertEqual(parsed?.uid, "mock_parent_uid") + XCTAssertEqual(parsed?.author_group.show_author, true) + XCTAssertEqual(parsed?.author_group.authors.first?.uid, "mock_referenced_uid") + XCTAssertEqual(parsed?.author_group.authors.first?.title, "Referenced Author Entry") + } + + // Codable models mirroring a 5-level chain: each level references the next. + private struct DeepL1: Decodable { let uid: String; let level_2: [DeepL2] } + private struct DeepL2: Decodable { let uid: String; let level_3: [DeepL3] } + private struct DeepL3: Decodable { let uid: String; let level_4: [DeepL4] } + private struct DeepL4: Decodable { let uid: String; let level_5: [DeepL5] } + private struct DeepL5: Decodable { let uid: String; let title: String } + + /// Verifies toJSON() flattens references nested 5 levels deep (e.g. include_all_depth=5). + /// The recursion follows the resolved object graph regardless of depth. + func testEntryModel_toJSON_deeplyNestedReferences_5Levels() throws { + // entry1 → entry2 → entry3 → entry4 → entry5, each resolved as a nested entry. + let apiResponseJSON = """ + { + "uid": "uid_1", "title": "Level 1", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "level_2": [{ + "uid": "uid_2", "title": "Level 2", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "level_3": [{ + "uid": "uid_3", "title": "Level 3", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "level_4": [{ + "uid": "uid_4", "title": "Level 4", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "level_5": [{ + "uid": "uid_5", "title": "Level 5", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user" + }] + }] + }] + }] + } + """ + + let data = apiResponseJSON.data(using: .utf8)! + let entry = try JSONDecoder.dateDecodingStrategy().decode(EntryModel.self, from: data) + + // The deepest reference is decoded as an EntryModel, so raw fields are not serializable. + XCTAssertFalse(JSONSerialization.isValidJSONObject(entry.fields ?? [:])) + + // toJSON() flattens all 5 levels into JSON-native types. + let normalized = entry.toJSON() + XCTAssertTrue(JSONSerialization.isValidJSONObject(normalized)) + + // Round-trip the full 5-level chain and assert the deepest entry's data survived. + let parsed: DeepL1? = DecodableTest.parseModel(normalized) + XCTAssertNotNil(parsed) + let level5 = parsed? + .level_2.first? + .level_3.first? + .level_4.first? + .level_5.first + XCTAssertEqual(level5?.uid, "uid_5") + XCTAssertEqual(level5?.title, "Level 5") + } + + /// An asset (file/image) field also decodes into a non-JSON `AssetModel` inside fields. + /// toJSON() must flatten it the same way it flattens nested entries. + func testEntryModel_toJSON_assetReference_isJSONSerializable() throws { + let apiResponseJSON = """ + { + "uid": "mock_parent_uid", "title": "Entry With Image", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "hero_image": { + "uid": "mock_asset_uid", + "title": "hero.png", + "filename": "hero.png", + "url": "https://images.contentstack.io/mock/hero.png", + "content_type": "image/png", + "file_size": "20480", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user" + } + } + """ + + let data = apiResponseJSON.data(using: .utf8)! + let entry = try JSONDecoder.dateDecodingStrategy().decode(EntryModel.self, from: data) + + XCTAssertTrue(entry.fields?["hero_image"] is AssetModel) + XCTAssertFalse(JSONSerialization.isValidJSONObject(entry.fields ?? [:])) + + let normalized = entry.toJSON() + XCTAssertTrue(JSONSerialization.isValidJSONObject(normalized)) + + let asset = normalized["hero_image"] as? [String: Any] + XCTAssertEqual(asset?["uid"] as? String, "mock_asset_uid") + XCTAssertEqual(asset?["url"] as? String, "https://images.contentstack.io/mock/hero.png") + } + + /// An entry containing BOTH a nested entry reference and an asset reference. + func testEntryModel_toJSON_mixedEntryAndAssetReferences() throws { + let apiResponseJSON = """ + { + "uid": "mock_parent_uid", "title": "Mixed Entry", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "related_entry": [{ + "uid": "mock_ref_uid", "title": "Related", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user" + }], + "attachment": { + "uid": "mock_asset_uid", "title": "file.pdf", "filename": "file.pdf", + "url": "https://images.contentstack.io/mock/file.pdf", "content_type": "application/pdf", + "file_size": "10240", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user" + } + } + """ + + let data = apiResponseJSON.data(using: .utf8)! + let entry = try JSONDecoder.dateDecodingStrategy().decode(EntryModel.self, from: data) + + // Both reference types present as non-JSON Swift objects. + let relatedArray = entry.fields?["related_entry"] as? [Any] + XCTAssertTrue(relatedArray?.first is EntryModel) + XCTAssertTrue(entry.fields?["attachment"] is AssetModel) + XCTAssertFalse(JSONSerialization.isValidJSONObject(entry.fields ?? [:])) + + let normalized = entry.toJSON() + XCTAssertTrue(JSONSerialization.isValidJSONObject(normalized)) + + let related = (normalized["related_entry"] as? [Any])?.first as? [String: Any] + XCTAssertEqual(related?["uid"] as? String, "mock_ref_uid") + let attachment = normalized["attachment"] as? [String: Any] + XCTAssertEqual(attachment?["uid"] as? String, "mock_asset_uid") + } + + /// toJSON() on an entry with no resolved references is a no-op that stays serializable. + func testEntryModel_toJSON_noReferences_isUnchangedAndSerializable() throws { + let apiResponseJSON = """ + { + "uid": "mock_uid", "title": "Plain Entry", "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user", + "body": "Some text", "views": 10 + } + """ + + let data = apiResponseJSON.data(using: .utf8)! + let entry = try JSONDecoder.dateDecodingStrategy().decode(EntryModel.self, from: data) + + // No references → raw fields are already JSON-native. + XCTAssertTrue(JSONSerialization.isValidJSONObject(entry.fields ?? [:])) + + let normalized = entry.toJSON() + XCTAssertTrue(JSONSerialization.isValidJSONObject(normalized)) + XCTAssertEqual(normalized["body"] as? String, "Some text") + XCTAssertEqual(normalized["views"] as? Int, 10) + } + + func testEntryModel_toJSON_preservesScalarValues() throws { + let jsonString = """ + { + "uid": "mock_uid", + "title": "Test Entry", + "locale": "en-us", + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", + "updated_by": "mock_user", + "is_active": true, + "count": 42, + "name": "Test" + } + """ + + let data = jsonString.data(using: .utf8)! + let entry = try JSONDecoder.dateDecodingStrategy().decode(EntryModel.self, from: data) + let json = entry.toJSON() + + XCTAssertEqual(json["uid"] as? String, "mock_uid") + XCTAssertEqual(json["title"] as? String, "Test Entry") + XCTAssertEqual(json["is_active"] as? Bool, true) + XCTAssertEqual(json["count"] as? Int, 42) + XCTAssertEqual(json["name"] as? String, "Test") + XCTAssertNoThrow(try JSONSerialization.data(withJSONObject: json)) + } + static var allTests = [ ("testJSONDecoder_dateDecodingStrategy", testJSONDecoder_dateDecodingStrategy), ("testJSONDecoder_dateDecodingStrategy_withISO8601Date", testJSONDecoder_dateDecodingStrategy_withISO8601Date), @@ -438,7 +704,13 @@ class DecodableTest: XCTestCase { ("testKeyedDecodingContainer_decodeArray", testKeyedDecodingContainer_decodeArray), ("testKeyedDecodingContainer_decodeIfPresentArray", testKeyedDecodingContainer_decodeIfPresentArray), ("testKeyedDecodingContainer_decodeNestedDictionaries", testKeyedDecodingContainer_decodeNestedDictionaries), - ("testKeyedDecodingContainer_decodeMixedTypes", testKeyedDecodingContainer_decodeMixedTypes) + ("testKeyedDecodingContainer_decodeMixedTypes", testKeyedDecodingContainer_decodeMixedTypes), + ("testEntryModel_includeAll_referenceFieldIsEntry_parsesViaToJSON", testEntryModel_includeAll_referenceFieldIsEntry_parsesViaToJSON), + ("testEntryModel_toJSON_deeplyNestedReferences_5Levels", testEntryModel_toJSON_deeplyNestedReferences_5Levels), + ("testEntryModel_toJSON_assetReference_isJSONSerializable", testEntryModel_toJSON_assetReference_isJSONSerializable), + ("testEntryModel_toJSON_mixedEntryAndAssetReferences", testEntryModel_toJSON_mixedEntryAndAssetReferences), + ("testEntryModel_toJSON_noReferences_isUnchangedAndSerializable", testEntryModel_toJSON_noReferences_isUnchangedAndSerializable), + ("testEntryModel_toJSON_preservesScalarValues", testEntryModel_toJSON_preservesScalarValues) ] } From 053860bcc0c482c533d0cfed760e9a6acaa707d8 Mon Sep 17 00:00:00 2001 From: reeshika-h Date: Fri, 12 Jun 2026 12:39:18 +0530 Subject: [PATCH 3/3] test: add unit test for AssetModel.toJSON() to verify JSON serialization --- Tests/DecodableTest.swift | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/DecodableTest.swift b/Tests/DecodableTest.swift index 0e0369aa..c4a958fb 100644 --- a/Tests/DecodableTest.swift +++ b/Tests/DecodableTest.swift @@ -595,6 +595,32 @@ class DecodableTest: XCTestCase { XCTAssertEqual(asset?["url"] as? String, "https://images.contentstack.io/mock/hero.png") } + /// A standalone asset (e.g. fetched via the assets endpoint) round-trips through toJSON(). + func testAssetModel_toJSON_isJSONSerializable() throws { + let apiResponseJSON = """ + { + "uid": "mock_asset_uid", + "title": "hero.png", + "filename": "hero.png", + "url": "https://images.contentstack.io/mock/hero.png", + "content_type": "image/png", + "file_size": "20480", + "created_at": "2024-01-01T00:00:00.000Z", "updated_at": "2024-01-01T00:00:00.000Z", + "created_by": "mock_user", "updated_by": "mock_user" + } + """ + + let data = apiResponseJSON.data(using: .utf8)! + let asset = try JSONDecoder.dateDecodingStrategy().decode(AssetModel.self, from: data) + + let normalized = asset.toJSON() + XCTAssertTrue(JSONSerialization.isValidJSONObject(normalized)) + XCTAssertEqual(normalized["uid"] as? String, "mock_asset_uid") + XCTAssertEqual(normalized["filename"] as? String, "hero.png") + XCTAssertEqual(normalized["url"] as? String, "https://images.contentstack.io/mock/hero.png") + XCTAssertNoThrow(try JSONSerialization.data(withJSONObject: normalized)) + } + /// An entry containing BOTH a nested entry reference and an asset reference. func testEntryModel_toJSON_mixedEntryAndAssetReferences() throws { let apiResponseJSON = """ @@ -708,6 +734,7 @@ class DecodableTest: XCTestCase { ("testEntryModel_includeAll_referenceFieldIsEntry_parsesViaToJSON", testEntryModel_includeAll_referenceFieldIsEntry_parsesViaToJSON), ("testEntryModel_toJSON_deeplyNestedReferences_5Levels", testEntryModel_toJSON_deeplyNestedReferences_5Levels), ("testEntryModel_toJSON_assetReference_isJSONSerializable", testEntryModel_toJSON_assetReference_isJSONSerializable), + ("testAssetModel_toJSON_isJSONSerializable", testAssetModel_toJSON_isJSONSerializable), ("testEntryModel_toJSON_mixedEntryAndAssetReferences", testEntryModel_toJSON_mixedEntryAndAssetReferences), ("testEntryModel_toJSON_noReferences_isUnchangedAndSerializable", testEntryModel_toJSON_noReferences_isUnchangedAndSerializable), ("testEntryModel_toJSON_preservesScalarValues", testEntryModel_toJSON_preservesScalarValues)