Home > Back-end >  Converting a POCO object with JsonPropertyName decorations into a URL query string
Converting a POCO object with JsonPropertyName decorations into a URL query string

Time:05-09

Is there an .NET API to convert a POCO object with JsonPropertyName decorations into a properly encoded URL query string?

For example, for:

public record AuthEndPointArgs
{
    [JsonPropertyName("response_type")]
    public string? ResponseType { get; set; } // "code"

    [JsonPropertyName("client_id")]
    public string? ClientId { get; set; } // "a:b"

    // ...
}

I expect: ?response_type=code&client_id=a:b.

A home-grown version I'm using for now:

    public static string ConvertShallowObjectToQueryString(object source, string query)
    {
        var uriArgs = System.Web.HttpUtility.ParseQueryString(query);

        // JsonPropertyName decorations become query string arguments
        var jsonNode = System.Text.Json.JsonSerializer.SerializeToNode(source) ??
            throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));

        foreach (var item in jsonNode.AsObject())
        {
            uriArgs[item.Key] = item.Value?.ToString() ?? String.Empty;
        }

        return uriArgs.ToString() ?? String.Empty;
    }

CodePudding user response:

one possibility is System.Text.Json.Serialization.JsonConverter<T> documented in How to customize property names and values with System.Text.Json

class AuthEndpointMarshaller : JsonConverter<AuthEndPointArgs>
{
    public override AuthEndPointArgs? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        //TODO check typeToConvert
        AuthEndPointArgs result = new();
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException();

        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.EndObject)
            {
                return result;
            }

            if (reader.TokenType != JsonTokenType.PropertyName)
                throw new JsonException();

            string ? propertyName = reader.GetString();
            reader.Read();
            string? propertyValue = reader.GetString();

            switch (propertyName)
            {
                case "response_type":
                    result.ResponseType = propertyValue;
                    break;
                case "client_id":
                    result.ClientId = HttpUtility.UrlDecode(propertyValue);
                    break;
            }
        }
        throw new JsonException();
    }

    public override void Write(Utf8JsonWriter writer, AuthEndPointArgs value, JsonSerializerOptions options)
    {
        value.ClientId = HttpUtility.UrlEncode(value.ClientId);
        JsonSerializer.Serialize(writer,value);
    }
}

You then call it using

string ConvertShallowObjectToQueryStringAlternate(AuthEndPointArgs source)
{
    var options = new JsonSerializerOptions()
    {
        Converters =
        {
            new AuthEndpointMarshaller()
        }
    };

    var jsonNode = JsonSerializer.SerializeToNode(source, options) ??
        throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
    return jsonNode.ToString();
}

or if you are looking for a quick solution

public record AuthEndPointArgs
{
    // ...
    public string ToQueryParams()
    {
        var sb = new StringBuilder($"?response_type={ResponseType}&client_id={HttpUtility.UrlEncode(ClientId)}");

        return sb.ToString();
    }
}

CodePudding user response:

To my knowledge there is no native .NET API, it seems like ASP.NET (Core) has support for reading it to some extent though (check here and here) but I can't tell how to create one.

The laziest solution would probably be to just serialize your object to JSON, and then HttpUtility.UrlEncode(json), then pass that to a query param, which would like so:

&payload={"response_type": "code","client_id": "a:b"}

At the other end just JsonSerializer.Deserialize<AuthEndPointArgs>(HttpUtility.UrlDecode(payload)) like so. This is assuming you can edit both ends.

While it sounds kinda stupid, it works, at in certain terms may even be better than serializing your AuthEndPointArgs to a query string directly, because the standard for a query string lacks some definitions, like how to deal with arrays, also complex options. It seems like the JS and PHP community have unofficial standards, but they require a manual implementation on both ends. So we'll also need to roll our own "standard" and implementation, unless we say that we can only serialize an object that fulfils the following criteria:

  • No complex objects as properties
  • No lists/ arrays as properties

Side note: URLs have a maximum length depending on a lot of factors, and by sending complex objects via query parameters you may go above that limit pretty fast, see here for more on this topic. It may just be best to hardcode something like ToQueryParams like Ady suggested in their answer

If we do want a generic implementation that aligns with those criteria, our implementation is actually quite simple:

public static class QueryStringSerializer
{
    public static string Serialize(object source)
    {
        var props = source.GetType().GetProperties(
            BindingFlags.Instance | BindingFlags.Public
        );

        var output = new StringBuilder();

        foreach (var prop in props)
        {
            // You might want to extend this check, things like 'Guid'
            // serialize nicely to a query string but aren't primitive types
            if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string))
            {
                var value = prop.GetValue(source);
                if (value is null)
                    continue;

                output.Append($"{GetNameFromMember(prop)}={HttpUtility.UrlEncode(value.ToString())}");
            }
            else
                throw new NotSupportedException();
        }
    }
}

private static string GetNameFromMember(MemberInfo prop)
{
    string propName;

    // You could also implement a 'QueryStringPropertyNameAttribute'
    // if you want to be able to override the name given, for this you can basically copy the JSON attribute
    // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyNameAttribute.cs
    if (Attribute.IsDefined(prop, typeof(JsonPropertyNameAttribute)))
    {
        var attribute = Attribute.GetCustomAttribute(prop, typeof(JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
        if (attribute is null)
            propName = prop.Name;
        else
            propName = attribute.Name;
    }
    else
        propName = prop.Name;

    return propName;
}

If we want to support objects with enumerables as properties or with "complex" objects as members we need to define how to serialize them, something like

class Foo
{
    public int[] Numbers { get; set; }
}

Could be serialized to

?numbers[]=1&numbers[]=2

Or to a 1 indexed "list"

?numbers[1]=1&numbers[2]=2

Or to a comma delimited list

?numbers=1,2

Or just multiple of one instance = enumerable

?numbers=1&numbers=2

And probably a lot more formats. But all of these are framework/ implementation specific of whatever is receiving these calls as there is no official standard, and the same goes for something like

class Foo
{
    public AuthEndPointArgs Args { get; set; }

Could be

?args.response_type=code&args.client_id=a:b

And a bunch more different ways I can't be bothered to think off right now

  • Related