Custom property editors support
Discover how to customize the Content Delivery API's response for custom property editors.
Out of the box, the Delivery API supports custom property editors, ensuring they are rendered alongside the built-in ones in Umbraco. However, if the output generated by a property editor isn't optimal for a headless context, developers can customize the API response. This customization won't impact the Razor rendering, allowing developers to tailor the Content Delivery API response according to their specific requirements.
This article will demonstrate how to work with the IDeliveryApiPropertyValueConverter interface and implement custom property expansion for custom property editors.
Prerequisite
The examples in this article revolve around the fictional My.Custom.Picker property editor. This property editor stores the key of a single content item and is backed by a property value converter.
This article will not dive into the details of creating a custom property editor for Umbraco. Guidance on that can be found in these articles: Creating a Property Editor and Property Value Converters.
Implementation
To customize the output of a property value editor in the Delivery API, opt-in by implementing the IDeliveryApiPropertyValueConverter interface.
The code example below showcases the implementation of this interface in the property value converter for My.Custom.Picker. The focus will be on customizing the methods provided by the IDeliveryApiPropertyValueConverter, as they are responsible for customizing the Delivery API response.
Response model classes can be found near the end of this article.
The IsConverter() and GetPropertyValueType() methods are inherited from the PropertyValueConverterBase class, which is covered in the Property Value Converters article.
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
public class MyCustomPickerValueConverter(
IPublishedContentCache publishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder):
PropertyValueConverterBase, IDeliveryApiPropertyValueConverter
{
public override bool IsConverter(IPublishedPropertyType propertyType)
=> propertyType.EditorAlias.Equals("My.Custom.Picker");
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(Guid?);
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Elements;
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Snapshot;
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(DeliveryApiCustomPicker);
public object? ConvertIntermediateToDeliveryApiObject(
IPublishedElement owner,
IPublishedPropertyType propertyType,
PropertyCacheLevel referenceCacheLevel,
object? inter,
bool preview,
bool expanding)
{
if (inter is null)
{
return null;
}
return BuildDeliveryApiCustomPicker(inter, expanding);
}
private DeliveryApiCustomPicker? BuildDeliveryApiCustomPicker(object inter, bool expanding)
{
if (!Guid.TryParse(inter as string, out Guid id))
{
return null;
}
var content = publishedContentCache.GetById(id);
if (content is null)
{
return null;
}
return new DeliveryApiCustomPicker { Id = id };
}
}The Implementation of the IDeliveryApiPropertyValueConverter interface can be found in the following methods:
GetDeliveryApiPropertyCacheLevel(): This method specifies the cache level used for our property representation in the Delivery API response.GetDeliveryApiPropertyCacheLevelForExpansion(): This method specifies the cache level used for our property representation in the Delivery API response when property expansion is applied.GetDeliveryApiPropertyValueType(): This method defines the value type of the custom property output for the Delivery API response.ConvertIntermediateToDeliveryApiObject(): This method converts the value from the property editor to the desired custom object in a headless context.
In the given example, the content key (Guid value) is used when rendering with Razor. This is sufficient because Razor provides full access to the published content within the rendering context. In a headless context, we do not have the same access. To prevent subsequent round-trips to the server, we create a richer output model specifically for the Delivery API.
The following example request shows how our custom implementation is reflected in the resulting API response. In this case, our custom property editor is configured under the alias "pickedItem".
Sample Request
GET /umbraco/delivery/api/v2/content/item/blogSample Response
{
"name": "Blog",
"createDate": "2023-05-25T11:26:52.591927",
"updateDate": "2023-06-07T12:53:41.339963",
"route": {
"path": "/",
"startItem": {
"id": "88694917-ac4a-4cfa-aa82-d89a81940126",
"path": "blog"
}
},
"id": "88694917-ac4a-4cfa-aa82-d89a81940126",
"contentType": "blog",
"properties": {
"pickedItem": {
"id": "61df413c-02b2-480d-8cb5-8e3c35132838",
"itemDetails": null
}
},
"cultures": {}
}Property expansion support
Property expansion allows developers to conditionally add another level of detail to the Delivery API output. Usually, these additional details are "expensive" to retrieve (for example, requiring database access to populate). By applying property expansion, Umbraco developers provide the option for the caller of the API to opt-in explicitly to this "expensive" operation. From the caller's perspective, the alternative might be an even more expensive additional round-trip to the server.
In this example, property expansion is implemented within ConvertIntermediateToDeliveryApiObject(). By considering the value of the expanding parameter, the BuildDeliveryApiCustomPicker() method can be modified as follows:
public class MyCustomPickerValueConverter(
IPublishedContentCache publishedContentCache,
IApiContentRouteBuilder apiContentRouteBuilder):
PropertyValueConverterBase, IDeliveryApiPropertyValueConverter
{
// ...
private DeliveryApiCustomPicker? BuildDeliveryApiCustomPicker(object inter, bool expanding)
{
if (!Guid.TryParse(inter as string, out Guid id))
{
return null;
}
// Property expansion support
if (!expanding)
{
return new DeliveryApiCustomPicker { Id = id };
}
var content = publishedContentCache.GetById(id);
if (content is null)
{
return new DeliveryApiCustomPicker { Id = id };
}
var route = apiContentRouteBuilder.Build(content);
var itemDetails = new DeliveryApiItemDetails
{
Name = content.Name,
Route = route
};
return new DeliveryApiCustomPicker { Id = id, ItemDetails = itemDetails };
}
}If the expanding parameter is false, the method returns the same shallow representation of the referenced content item as before. Otherwise, retrieve the corresponding IPublishedContent and construct the response object accordingly.
To see the expanded output in the API response, add the expand query parameter to the request. This parameter can be applied using either ?expand=properties[$all] to expand all properties or ?expand=properties[pickedItem] to expand the specific 'pickedItem' property.
Sample Request
GET /umbraco/delivery/api/v2/content/item/blog?expand=properties[pickedItem]Sample Response
{
"name": "Blog",
"createDate": "2023-05-25T11:26:52.591927",
"updateDate": "2023-06-07T12:53:41.339963",
"route": {
"path": "/",
"startItem": {
"id": "88694917-ac4a-4cfa-aa82-d89a81940126",
"path": "blog"
}
},
"id": "88694917-ac4a-4cfa-aa82-d89a81940126",
"contentType": "blog",
"properties": {
"pickedItem": {
"id": "61df413c-02b2-480d-8cb5-8e3c35132838",
"itemDetails": {
"name": "Codegarden",
"createDate": "2023-05-21T00:34:11.513868",
"updateDate": "2023-06-06T17:46:12.833186",
"route": {
"path": "/codegarden/",
"startItem": {
"id": "88694917-ac4a-4cfa-aa82-d89a81940126",
"path": "blog"
}
}
}
}
},
"cultures": {}
}The itemDetails property of the pickedItem in the JSON response contains the additional details of the selected content item.
Supporting Model Classes
public class DeliveryApiCustomPicker
{
public Guid Id { get; set; }
public DeliveryApiItemDetails? ItemDetails { get; set; }
}
public class DeliveryApiItemDetails
{
public string? Name { get; set; }
public IApiContentRoute? Route { get; set; }
}Last updated
Was this helpful?