Pages

Tuesday, 27 November 2018

Umbraco Headless - Optional Images

When you use the Umbraco.Headless.Client on top of an Umbraco Headless instance, you are accessing Umbraco through a REST API. This means your content is serialized (to JSON) on the Umbraco side and deserialized inside the Headless-client.

BUT : I ran into a problem when using optional Images.

First steps

Create a very simple Document Type, with 2 properties.


Create a class in your Visual Studio project that maps to this DocumentType.

using Umbraco.Headless.Client.Net.Models;

namespace Models
{
    public class Test : ContentItem, IContentBase
    {
        public string Title { get; set; }
        public string Description { get; set; }
    }
}

Fetch a node that uses this template. The quickest way is to get it via its Id.
Normally, the contentService would be injected, but for simplicity's sake, let's keep it like this.

  var umbracoHeadlessUrl = ConfigurationManager.AppSettings[umbracoHeadless:url];
  var contentService =  new PublishedContentService(umbracoHeadlessUrl);

  var testNode = await contentService.GetById<Test>(id);

The resulting JSON looks like this :

{
    "Title": "Title 1",
    "Description": "<p>A very long description.</p>",
    "id": "1940908b-460c-47dd-825a-010381a57abd",
    "parentId": "7a0e6214-7802-4130-b77c-0965148b0a33",
    "name": "Test",
    "path": null,
    "contentTypeAlias": "test",
    "createDate": "0001-01-01T00:00:00",
    "updateDate": "0001-01-01T00:00:00",
    "url": null,
    "writerName": null,
    "creatorName": null,
    "properties": null,
    "_links": null
}

Adding an Image

Add a property to your template using datatype MediaPicker.
Add a property to your Test-class of type Image. I'm only interested in the Url-property of this image, so that's the only one I created.

public class Image
{
    public string Url { get; set; }
}
public class Test : ContentItem, IContentBase
{
    public string Title { get; set; }
    public string Description { get; set; }
    public Image Image { get; set; }
}

When I leave the Image-property empty in Umbraco and run my changes, I get the following error:

Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'Models.Image' 
because the type requires a JSON object (e.g. {\"name\":\"value\"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {\"name\":\"value\"}) or change the deserialized 
type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> 
that can be deserialized from a JSON array.


The reason for this error, is that the JSON for the empty image is

    "image": [],

which would require your property to become a List<Image> instead.
However, if you put something in the Image property, the JSON comes out as

    "image": {
        "writerName": "Jeroen Vantroyen",
        "creatorName": "",
        "writerId": 1,
        "creatorId": 1,
        "urlName": "question",
        "url": "/media/1001/images.png",
        ...
        //truncated for brevity
    },

So, when left empty, it's an (empty) array. When filled in, it's a simple JSON object.
How to fix this?

JsonConvertor to the rescue

The solution is simple and elegant, using a JsonConvertor, which can handle both arrays and single objects.
I do not take credit for the solution below, but will save you the hunt for the code.

    public class SingleOrArrayConverter<T> : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return (objectType == typeof(List<T>));
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JToken token = JToken.Load(reader);
            if (token.Type == JTokenType.Array)
            {
                return token.ToObject<List<T>>();
            }
            return new List<T> { token.ToObject<T>() };
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            List<T> list = (List<T>)value;
            if (list.Count == 1)
            {
                value = list[0];
            }
            serializer.Serialize(writer, value);
        }

        public override bool CanWrite
        {
            get { return true; }
        }
    }

To use this, I modify my test-class one last time.

public class Test : ContentItem, IContentBase
{
    public string Title { get; set; }
    public string Description { get; set; }

    [JsonConverter(typeof(SingleOrArrayConverter<Image>))]
    public List<Image> Image { get; set; }
}

To use an instance of this class, I would resort to Image.FirstOrDefault().

No comments:

Post a Comment