NikiforovAll/keycloak-authorization-services-dotnet

Attributes field no longer dictionary but object

HOTAPIYC opened this issue · 4 comments

Hi,

many of the component schemas of the Keycloak OpenAPI spec contain a field named "attributes", which contain a dictionary that contains arrays of strings, specified like so:

"UserRepresentation": {
    "attributes": {
        "type": "object",
        "additionalProperties": {
            "type": "array",
            "items": {
            "type": "string"
        }
    }
}

To my understanding this translates to a Dictionary<string, string[]> or similar, which was also the case in the previous major version of this library: User

Now I really appreciate the introduction of the generated client, featuring all possible endpoints, for which I previously had to manually extend this library and which now works out-of-the-box like a charm. These attributes however are now of some different type: UserRepresentation.

As far as I understand this because of how Kiota handles the snippet above. Is there a way to restore the previous serialization? Or am I missing something else? Thanks!

Thank you for the feedback. It is the way the model is described in OpenAPI Schema so there is no way to override it since the client is automatically generated. I think you can still use UserRepresentation_attributes since it contains field AdditionalData of type Dictionary<string, object>. The only thing left is to cast object to string[] and it should work I believe.

Thanks for the quick response! To add some context: this is for a web api that wraps the keycloak admin api which basically returns or accepts a subset of the same entities. The server stub for this web api is generated using the openapi csharp generator, reading the same openapi component schemas from the keycloak spec. As it turned out, the two generators do not return the same results.

I figured out a way to utilize Automapper though, that I am using within my project anyways. I added a custom converter for reading:

public class AdditionalDataConverter<T> : ITypeConverter<T, Dictionary<string, List<string>>>
    where T : IAdditionalDataHolder
{
    public Dictionary<string, List<string>> Convert(
        T source,
        Dictionary<string, List<string>> destination,
        ResolutionContext context)
    {
        return source?.AdditionalData?.ToDictionary(
            x => x.Key,
            x =>
            {
                var values = ((UntypedArray) x.Value).GetValue();
                return values.Select(x => ((UntypedString) x).GetValue()).ToList();
            });
    }
}

And for writing:

public class AttributesConverter<T> : ITypeConverter<Dictionary<string, List<string>>, T>
    where T : IAdditionalDataHolder, new()
{
    public T Convert(
        Dictionary<string, List<string>> source,
        T destination,
        ResolutionContext context)
    {
        var attributes = new T();
        attributes.AdditionalData = source.ToDictionary(
            x => x.Key,
            x => (object) x.Value);

        return attributes;
    }
}

This is added to the profiles e.g. like so:

public class EntityProfiles : Profile
{
    public EntityProfiles()
    {       
        CreateMap<Dictionary<string, List<string>>, UserRepresentation_attributes>()
            .ConvertUsing<AttributesConverter<UserRepresentation_attributes>>();
        CreateMap<Dictionary<string, List<string>>, GroupRepresentation_attributes>()
            .ConvertUsing<AttributesConverter<GroupRepresentation_attributes>>();
        CreateMap<Dictionary<string, List<string>>, ClientRepresentation_attributes>()
            .ConvertUsing<AttributesConverter<ClientRepresentation_attributes>>();
    }
}

I would have preferred doing it without, but I guess this will do the job for now. Thanks for your effort anyways!

Thank you for the added context, I think it might be useful for others. 🙌

I think you can go even further and scan for IAdditionalDataHolder and register converters programmatically based on assembly scanning results to avoid this situation in the future if needed

@HOTAPIYC