Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### Version: 1.4.0
#### Date: June-23-2026
- Added `EmbeddedObject` as a concrete implementation of `IEmbeddedObject`, covering both `IEmbeddedEntry` and `IEmbeddedAsset`.
- Added `EmbeddedObjectConverter` to resolve `IEmbeddedObject` during JSON deserialization without requiring changes in consumer code.
- Custom fields on embedded entries and assets are preserved via `[JsonExtensionData]`.

### Version: 1.3.0

#### Date: May-11-2026
Expand Down
400 changes: 400 additions & 0 deletions Contentstack.Utils.Tests/EmbeddedObjectConverterTest.cs

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Contentstack.Utils.Tests/Resources/embeddedAsset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"uid": "sample_asset_uid",
"_content_type_uid": "sys_assets",
"title": "Dummy Image Asset",
"filename": "dummy-image.jpg",
"url": "https://example.com/assets/dummy-image.jpg",
"content_type": "image/jpeg",
"file_size": "10000",
"locale": "en-us",
"is_dir": false,
"tags": ["test", "dummy"],
"dimension": {
"height": 100,
"width": 100
},
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
}
23 changes: 23 additions & 0 deletions Contentstack.Utils.Tests/Resources/embeddedEntry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"uid": "sample_author_uid",
"_content_type_uid": "author",
"title": "Dummy User",
"locale": "en-us",
"_version": 1,
"created_at": "2021-01-01T00:00:00.000Z",
"updated_at": "2021-01-01T00:00:00.000Z",
"bio": "This is a dummy bio used for testing purposes.",
"email": "dummy.user@example.com",
"avatar_url": "https://example.com/dummy-avatar.jpg",
"social": {
"twitter": "@dummyuser",
"linkedin": "linkedin.com/in/dummyuser"
},
"tags": ["test", "dummy", "sample"],
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
}
67 changes: 67 additions & 0 deletions Contentstack.Utils.Tests/Resources/embeddedItems.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[
{
"uid": "sample_author_uid",
"_content_type_uid": "author",
"title": "Dummy User",
"locale": "en-us",
"_version": 1,
"bio": "This is a dummy bio used for testing purposes.",
"email": "dummy.user@example.com",
"avatar_url": "https://example.com/dummy-avatar.jpg",
"social": {
"twitter": "@dummyuser",
"linkedin": "linkedin.com/in/dummyuser"
},
"tags": ["test", "dummy", "sample"],
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
},
{
"uid": "sample_asset_uid",
"_content_type_uid": "sys_assets",
"title": "Dummy Image Asset",
"filename": "dummy-image.jpg",
"url": "https://example.com/assets/dummy-image.jpg",
"content_type": "image/jpeg",
"file_size": "10000",
"locale": "en-us",
"is_dir": false,
"tags": ["test", "dummy"],
"dimension": {
"height": 100,
"width": 100
},
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
},
{
"uid": "sample_asset_uid_2",
"_content_type_uid": "sys_assets",
"title": "Dummy PNG Asset",
"filename": "dummy-image.png",
"url": "https://example.com/assets/dummy-image.png",
"content_type": "image/png",
"file_size": "5000",
"locale": "en-us",
"is_dir": false,
"tags": ["test", "dummy"],
"dimension": {
"height": 100,
"width": 100
},
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid_2"
}
}
]
134 changes: 134 additions & 0 deletions Contentstack.Utils.Tests/Resources/rteEntryWithEmbeddedItems.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
"entry": {
"uid": "sample_rte_entry_uid",
"_content_type_uid": "rte_json_comp",
"title": "Test RTE Entry",
"locale": "en-us",
"_version": 1,
"created_at": "2021-01-01T00:00:00.000Z",
"updated_at": "2021-01-01T00:00:00.000Z",
"author_name": "Test Author",
"category": "Test Category",
"rte_json": {
"type": "doc",
"children": [
{
"type": "p",
"children": [
{ "text": "Featured author: " },
{
"type": "reference",
"attrs": {
"type": "entry",
"entry-uid": "sample_author_uid",
"content-type-uid": "author",
"locale": "en-us",
"display-type": "inline"
},
"children": [{ "text": "" }]
}
]
},
{
"type": "p",
"children": [{ "text": "Sample image below:" }]
},
{
"type": "reference",
"attrs": {
"type": "asset",
"asset-uid": "sample_asset_uid",
"content-type-uid": "sys_assets",
"display-type": "display"
},
"children": [{ "text": "" }]
},
{
"type": "reference",
"attrs": {
"type": "asset",
"asset-uid": "sample_asset_uid_2",
"content-type-uid": "sys_assets",
"display-type": "display"
},
"children": [{ "text": "" }]
}
]
},
"_embedded_items": {
"rte_json": [
{
"uid": "sample_author_uid",
"_content_type_uid": "author",
"title": "Dummy User",
"locale": "en-us",
"_version": 1,
"bio": "This is a dummy bio used for testing purposes.",
"email": "dummy.user@example.com",
"avatar_url": "https://example.com/dummy-avatar.jpg",
"social": {
"twitter": "@dummyuser",
"linkedin": "linkedin.com/in/dummyuser"
},
"tags": ["test", "dummy", "sample"],
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
},
{
"uid": "sample_asset_uid",
"_content_type_uid": "sys_assets",
"title": "Dummy Image Asset",
"filename": "dummy-image.jpg",
"url": "https://example.com/assets/dummy-image.jpg",
"content_type": "image/jpeg",
"file_size": "10000",
"locale": "en-us",
"is_dir": false,
"tags": ["test", "dummy"],
"dimension": {
"height": 100,
"width": 100
},
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
},
{
"uid": "sample_asset_uid_2",
"_content_type_uid": "sys_assets",
"title": "Dummy PNG Asset",
"filename": "dummy-image.png",
"url": "https://example.com/assets/dummy-image.png",
"content_type": "image/png",
"file_size": "5000",
"locale": "en-us",
"is_dir": false,
"tags": ["test", "dummy"],
"dimension": {
"height": 100,
"width": 100
},
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid_2"
}
}
]
},
"publish_details": {
"environment": "test",
"locale": "en-us",
"time": "2021-01-01T00:00:00.000Z",
"user": "sample_user_uid"
}
}
}
34 changes: 34 additions & 0 deletions Contentstack.Utils/Converters/EmbeddedObjectConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using Contentstack.Utils.Interfaces;
using Contentstack.Utils.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Contentstack.Utils.Converters
{
// Resolves IEmbeddedObject to EmbeddedObject during deserialization.
// CanConvert uses exact type match so customer-defined subclasses are not intercepted.
public class EmbeddedObjectConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(IEmbeddedObject);

public override bool CanWrite => false;

public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;

var jo = JObject.Load(reader);
var result = new EmbeddedObject();
serializer.Populate(jo.CreateReader(), result);
return result;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> throw new NotSupportedException(
"EmbeddedObjectConverter is read-only. Serialize EmbeddedObject directly.");
}
}
30 changes: 30 additions & 0 deletions Contentstack.Utils/Models/EmbeddedObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Collections.Generic;
using Contentstack.Utils.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Contentstack.Utils.Models
{
// Concrete class used by EmbeddedObjectConverter when deserializing _embedded_items.
// Implements both IEmbeddedEntry and IEmbeddedAsset to cover entries and assets.
public class EmbeddedObject : IEmbeddedEntry, IEmbeddedAsset
{
[JsonProperty("uid")]
public string Uid { get; set; } = string.Empty;

[JsonProperty("_content_type_uid")]
public string ContentTypeUid { get; set; } = string.Empty;

[JsonProperty("title")]
public string Title { get; set; } = string.Empty;

[JsonProperty("filename")]
public string FileName { get; set; } = string.Empty;

[JsonProperty("url")]
public string Url { get; set; } = string.Empty;

// Any field not explicitly declared above (custom fields, locale data, etc.)
[JsonExtensionData]
public IDictionary<string, JToken> Fields { get; set; } = new Dictionary<string, JToken>();
}
}
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.3.0</Version>
<Version>1.4.0</Version>
</PropertyGroup>
</Project>
Loading