Serialize Complex Data for Sitecore XP Headless Development
In Sitecore headless development, the layout service automatically provides you with the serialized data for the current page, rendering, etc., giving you the field values to render your data. However, if you’ve worked with Sitecore for a long time, you occasionally have to do complex transformations on item fields before rendering them. As a simple example, say you have Department landing pages, and on every child page, you want to display the name of the parent Department in the header. In Sitecore Model View Controller (MVC), you could do this easily using Sitecore.Context.Database and Sitecore.Context.Item.
However, in headless development, this is more complicated. You don’t have direct access to the database since the rendering host is decoupled from the content management (CM) server. You must use GraphQL or a custom API to run queries on item IDs and get the field data from those additional items. GraphQL is a crucial and powerful tool for headless development, but in some cases, it’s a lot easier to compute data on the backend and have it immediately available in the layout service (not to mention that there are use limits on GraphQL queries to keep in mind).
In our first headless Sitecore XP 10.3 project, we came up with two ways to provide custom computed data through the layout service as an item field.
Note: This only applies to our experience with XP and XM. This is likely not applicable to XM Cloud, as XM Cloud’s Sitecore functionality is not intended to be customized.
Calculated Fields on Item:Saving
One option is to create a field that automatically gets written to when the item is saved. The process for this is no different from previous versions of Sitecore: create a custom processor and add a patch to add it to the item:saving pipeline.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/"
xmlns:set="http://www.sitecore.net/xmlconfig/set/">
<sitecore>
<events>
<event name="item:saving">
<handler patch:after="handler[@type='Sitecore.Tasks.ItemEventHandler, Sitecore.Kernel']" type="MySite.ItemSaving.CustomItemSaving, MySite" method="OnItemSaving" resolve="true"/>
</event>
</events>
</sitecore>
</configuration>
Then, in the custom processor, compute whatever you need and save it to a designated field on the item:
public class CustomItemSaving: ItemSavingBase
{
public void OnItemSaving(object sender, EventArgs args)
{
Item savingItem = Event.ExtractParameter(args, 0) as Item;
string computedData = GetComputedData(savingItem);
savingItem.Fields["customField"] = computedData;
}
}
In this example, GetComputedData(Item item) can be as complex or simple as you like. The value of the computedData field could be simple plain text or a more complex JSON array. Here are two examples from our solution, one simple and one more complex:
- Access the parent’s ancestors and get the first one that is a certain template (
Sitecore.Context.Item.Axes.GetAncestors().FirstOrDefault(x => x.TemplateName="Department")
). Then, if not null, return the name of the department. This provides us with the parent department name on every child page without a GraphQL query. - Create a JSON object to put a large amount of computed data in one single field. For example, we take all our linked Department page IDs and turn them into an array of objects with the display name, address, and phone number of each department; then serialize the array and save it to the custom field. Now we can easily access data that would otherwise have required an unknown number of GraphQL queries since we would need to run a query for each ID of the multi-list. As a bonus, we can now get this JSON data indexed as well and access it easily in our search results.
Custom Serialized Fields
Alternatively, you can use custom serialization to compute the field value in the layout service during the page request. This involves overriding the GetDefaultFieldSerializer pipeline and adding a custom text field serializer.
For this example, we’re going to use the department name again.
- Create a field on the item template named departmentName. This field will always remain blank in the content editor. Even if you entered a value, it would be overwritten.
- Override the GetDefaultFieldSerializer pipeline and add a custom TextFieldSerializer.
- In the TextFieldSerializer, if the current field matches a field name or ID, write a custom value; otherwise, fall back to the default behavior.
- The custom serializer will run on every field, so it’s crucial you only run your custom serialization on the intended field.
Here’s the setup for the configuration and default pipeline override:
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:search="http://www.sitecore.net/xmlconfig/search/">
<sitecore>
<pipelines>
<group groupName="layoutService">
<pipelines>
<getFieldSerializer>
<processor type="MySite.Foundation.LayoutService.Pipelines.GetFieldSerializer.CustomGetDefaultFieldSerializer, MySite" resolve="true"
patch:instead="*[@type='Sitecore.LayoutService.Serialization.Pipelines.GetFieldSerializer.GetDefaultFieldSerializer, Sitecore.LayoutService']" />
</getFieldSerializer>
</pipelines>
</group>
</pipelines>
</sitecore>
</configuration>
namespace MySite.Foundation.LayoutService.Pipelines.GetFieldSerializer
{
public class CustomGetDefaultFieldSerializer : GetDefaultFieldSerializer
{
public CustomGetDefaultFieldSerializer(IFieldRenderer fieldRenderer):base(fieldRenderer)
{
}
protected override void SetResult(GetFieldSerializerPipelineArgs args)
{
args.Result = (IFieldSerializer)new CustomTextFieldSerializer(this.FieldRenderer);
args.AbortPipeline();
}
}
}
Once that’s in place, add the custom text field serializer. This is where the actual magic happens!
namespace MySite.Foundation.LayoutService.Pipelines.GetFieldSerializer
{
public class CustomTextFieldSerializer : TextFieldSerializer
{
public CustomTextFieldSerializer(IFieldRenderer fieldRenderer):base(fieldRenderer)
{
}
protected override void WriteValue(Field field, JsonTextWriter writer)
{
if(TryWriteCustomField(field, writer))
{
// we handle the field our way
return;
}
base.WriteValue(field, writer);
}
private bool TryWriteCustomField(Field field, JsonTextWriter writer)
{
if(ID.IsNullOrEmpty(field?.ID))
{
return false;
}
try
{
if (field.Name == "departmentName")
{
var departmentName = GetComputedData(field?.Item);
writer.WriteValue(departmentName ?? string.Empty);
return true;
}
}
catch (Exception exp)
{
// our override didn't work - don't break Sitecore default functionality
}
return false;
}
}
}
As you add more custom fields to serialize, you can add the logic for each to TryWriteCustomField.
Which Approach to Serializing Complex Data Should You Use?
Both approaches we’ve shared in this post work well, so it’s up to you. The difference is that the item:saving version adds processing time during content authoring, while the serializer method adds processing time on request. So, in theory, the item:saving method is better for your website’s performance with visitors. If you’re using static site generation though, this doesn’t really make a difference to the user since all pages are generated at build time anyway. However, it can impact the build time for your site and costs more on publishing. We also like the item:saving method because you can see the data in Sitecore that is going to be served, rather than a blank field that “magically” gets populated with data when the rendering host calls it.
Have questions about either of these methods for serializing complex data in Sitecore XP headless development? Contact us! We would be happy to walk you through them or help you implement them for your site.