How to consume/deserialize strapi v4 endpoints in C#?

System Information
  • Strapi Version: 4.0.6
  • Operating System: Windows
  • Database: Postres
  • Node Version: v16.14.0
  • NPM Version: 8

I upgraded from v3 to v4 to only find there are tons of things to change in the data formats of the REST endpoints.
I consume the API in C#, using Newtonsoft.Json. Normally, if one endpoint returns single object or array of objects, I can share the same class to deserialize them.

{
   "name": "Lam",
   "age": 15
}

// or

[ { ... }, { ... } ........ ]

public class Info
{
  public string Name {get;set;}
  public int Age {get;set;}
}

// I would be using
// Info
// or List<Info>
// As simple as that

But now, they are nested under a prop called “data”. And the value of that property can be an object or array. In C#, I don’t think I can define a class to be either an object/class or a list of objects.
So, with the above example, how can I proceed without expanding things way too much…?

public class SingleObjectReturned {
  public Info Data { get; set; }
}

public class ManyObjectsReturned {
  public List<Info> Data { get; set; }
}

This is my intended solution. But it is cumbersome. Not to mention there will be nested relations of many types, one to one, or one to many…

Has anyone had migrated to v4 using C#? May you please share a hint on how to consume these endpoints in a… least painful way? Or what I said above is the correct way to handle this json deserialize?
Many thanks.

1 Like

Hey, not sure if you solved this yet but thought I’d share the way I did for anyone else probing this in the future.

create a few simple models:


    public class StrapiRoot<T> where T: AttributeWrapper {        
        [JsonProperty("data")]
        public List<DataWrapper<T>> data { get; set; }
        
        public List<T> datum {
            get
            {
                if(data == null) {
                    return new List<T>();
                }
                
                return data.Select(x => x.attributes).ToList();
            }
        }
    }

    public class DataWrapper<T> where T: AttributeWrapper {
        public int Id { get;set; }
        private T mAttributes;
        public T attributes 
        {
            get
            {
                return mAttributes;
            }

            set
            {
                value.id = Id;
                mAttributes = value;
            }
        }
    }

    public class AttributeWrapper {
        public int id { get; set; }
    }

then on your base class models, inherit from AttributeWrapper, and use StrapiRoot for your properties. Worked well for me so far. It’s a little messy, I unpack the attributes for each type using the “datum” property on root for easier access to the base models.

Hope it helps!

    public class ProductGroup : AttributeWrapper
    {
        public string ProductGroupName { get; set; }

        public string ProductGroupId { get; set; }
                
        [JsonProperty("products")]
        public StrapiRoot<Product> products { get; set; }

    }  
1 Like

needed support for array / non array, so created plural and singular roots

//use this if you come across a root object that is not an array
public class StrapiRoot<T> where T: AttributeWrapper {        
    [JsonProperty("data")]
    public DataWrapper<T> data { get; set; }
    
    public T datum {
        get
        {
            return data.attributes;
        }
    }
}

//use this for object which are array's at the root
public class StrapiPluralRoot<T> where T: AttributeWrapper {        
    [JsonProperty("data")]
    public List<DataWrapper<T>> data { get; set; }
    
    public List<T> datum {
        get
        {
            if(data == null) {
                return new List<T>();
            }
            
            return data.Select(x => x.attributes).ToList();
        }
    }
}
1 Like

Hello :slight_smile: I have a solution that proposes a System.Text.Json JsonConverter. It is using reflection, so I would not recommend using this code other than on a WebApi solution. Reflection may be slow on mobile platforms.

Solution

    // An abstract class for your items
    public abstract class StrapiItem<T>
        where T : StrapiItem<T>
    {
        public int Id { get; set; }

        public string Locale { get; set; }

        public DateTime? CreatedAt { get; set; }

        public DateTime? UpdatedAt { get; set; }

        public DateTime? PublishedAt { get; set; }

        public StrapiListWrapper<T> Localizations { get; set; }
    }
    // A wrapper for your lists
    public class StrapiListWrapper<T>
        where T : StrapiItem<T>
    {
        public IEnumerable<T> Data { get; set; }

        public Meta Meta { get; set; }
    }
    // A JsonConverter implementation for a StrapiItem<T>
    public class StrapiItemConverter<T> : JsonConverter<T>
        where T : StrapiItem<T>, new()
    {
        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            string? propertyName = null;
            var item = new T();
            var properties = typeToConvert.GetProperties();
            var initialDepth = reader.CurrentDepth;
            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    propertyName = reader.GetString();
                }
                else if (!string.IsNullOrWhiteSpace(propertyName))
                {
                    if (propertyName == "attributes" || reader.TokenType == JsonTokenType.Null)
                    {
                        continue;
                    }

                    var property = properties.SingleOrDefault(property => property.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
                    var setMethod = property?.GetSetMethod();
                    if (setMethod != null)
                    {
                        object? value = null;
                        if (typeof(int).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetInt32();
                        }
                        else if (typeof(string).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetString();
                        }
                        else if (typeof(DateTime).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetDateTime();
                        }
                        else if (typeof(DateTimeOffset).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetDateTimeOffset();
                        }
                        else if (typeof(bool).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetBoolean();
                        }
                        else if (typeof(decimal).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetDecimal();
                        }
                        else if (typeof(double).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetDouble();
                        }
                        else if (typeof(Guid).IsAssignableTo(property!.PropertyType))
                        {
                            value = reader.GetGuid();
                        }
                        else if (property.PropertyType.IsEnum || Nullable.GetUnderlyingType(property.PropertyType) != null)
                        {
                            var enumValue = reader.GetString();
                            if (!string.IsNullOrWhiteSpace(enumValue))
                            {
                                value = Enum.Parse(property.PropertyType, enumValue!, ignoreCase: true);
                            }
                        }
                        else
                        {
                            using (var jsonDocument = JsonDocument.ParseValue(ref reader))
                            {
                                var json = jsonDocument.RootElement.GetRawText();
                                value = JsonSerializer.Deserialize(json, property.PropertyType, options);
                            }
                        }

                        if (value != null)
                        {
                            setMethod.Invoke(item, new[] { value });
                        }
                    }

                    propertyName = null;
                }
                else if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == initialDepth)
                {
                    break;
                }
            }

            return item;
        }

        public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
        {
            throw new NotSupportedException();
        }
    }

Usage

Single items

Now, when strapi returns a single item like this…

{
    "data": {
        "id": 1,
        "attributes": {
            "title": "foo",
            "createdAt": "2023-04-19T00:20:53.175Z",
            "updatedAt": "2023-04-21T17:28:45.603Z",
            "publishedAt": "2023-04-19T00:24:22.336Z",
            "locale": "en",
        }
    },
    "meta": {}
}

… you can deserialize it like this :

var jsonSerializerOptions = new JsonSerializerOptions
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNameCaseInsensitive = true,
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var data = JsonSerializer.Deserialize<StrapiItemWrapper<Data>>(json, options);

And the data class would look something like this :

[JsonConverter(typeof(StrapiItemConverter<Data>))]
public class Data : StrapiItem<Data>
{
    public string Title { get; set; }
}

Lists

When strapi returns a list like this…

{
    "data": [
        {
            "id": 1,
            "attributes": {
                "title": "Mitre saws",
                "createdAt": "2023-04-19T00:20:53.175Z",
                "updatedAt": "2023-04-21T17:28:45.603Z",
                "publishedAt": "2023-04-19T00:24:22.336Z",
                "locale": "en",
            }
        }
    ]
}

…you can deserialize it like this :

var jsonSerializerOptions = new JsonSerializerOptions
{
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    PropertyNameCaseInsensitive = true,
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var data = JsonSerializer.Deserialize<StrapiListWrapper<Data>>(json, options);

I’m thinking of wrapping this into a nuget package or maybe make a pull request on this repo to help the .NET community use Strapi.

Let me know in the comments if you would be interested!

2 Likes