user3230660
user3230660

Reputation:

Set RootAttribute for System.Text.JsonSerializer?

Is there something like XmlRootAttribute that can be used with System.Text.JsonSerializer?

I need to be able to download data from this vendor using both XML an JSON. See sample data here:

{
    "categories": [
        {
            "id": 125,
            "name": "Trade Balance",
            "parent_id": 13
        }
    ]
}

Note the data elements are wrapped by an array named categories. When downloading using XML I can set the root element to categories and the correct object is returned (see XMLClient below ). When using JSONClient however I cannot (or do not know how) to set the root element. Best workaround I could find is to use JsonDocument which creates a string allocation. I could also create some wrapper classes for the JSON implementation but that is a lot of work that involves not only creating additional DTOs but also requires overwriting many methods on BaseClient. I also don't want to write converters - it pretty much defeats the purpose of using well-known serialization protocols. (Different responses will have a different root wrapper property name. I know the name in runtime, but not necessarily at compile time.)

public class JSONClient : BaseClient
{
    protected override async Task<T> Parse<T>(string uri, string root)
    {
        uri = uri + (uri.Contains("?") ? "&" : "?") + "file_type=json";
        var document = JsonDocument.Parse((await Download(uri)), new JsonDocumentOptions { AllowTrailingCommas = true });
        string json = document.RootElement.GetProperty(root).GetRawText(); // string allocation 
        return JsonSerializer.Deserialize<T>(json);
    }
}

public class XMLClient : BaseClient
{
    protected override async Task<T> Parse<T>(string uri, string root)
    {
        return (T)new XmlSerializer(typeof(T), new XmlRootAttribute(root)).Deserialize(await Download(uri)); // want to do this - using JsonSerializer
    }
}


public abstract class BaseClient
{
    protected virtual async Task<Stream> Download(string uri)
    {
        uri = uri + (uri.Contains("?") ? "&" : "?") + "api_key=" + "xxxxx";
        var response = await new HttpClient() { BaseAddress = new Uri(uri) }.GetAsync(uri);
        return await response.Content.ReadAsStreamAsync();
    }

    protected abstract Task<T> Parse<T>(string uri, string root) where T : class, new();

    public async Task<Category> GetCategory(string categoryID)
    {
        string uri = "https://api.stlouisfed.org/fred/category?category_id=" + categoryID;
        return (await Parse<List<Category>>(uri, "categories"))?.FirstOrDefault();
    }
}

Upvotes: 1

Views: 1721

Answers (1)

dbc
dbc

Reputation: 116980

JSON has no concept of a root element (or element names in general), so there's no equivalent to XmlRootAttribute in System.Text.Json (or Json.NET for that matter). Rather, it has the following two types of container, along with several atomic value types:

  • Objects, which are unordered sets of name/value pairs. An object begins with {left brace and ends with }right brace.

  • Arrays, which are ordered collections of values. An array begins with [left bracket and ends with ]right bracket.

As System.Text.Json.JsonSerializer is designed to map c# objects to JSON objects and c# collections to JSON arrays in a 1-1 manner, there's no built-in attribute or declarative option to tell the serializer to automatically descend the JSON hierarchy until a property with a specific name is encountered, then deserialize its value to a required type.

If you need to access some JSON data that is consistently embedded in some wrapper object containing a single property whose name is known at runtime but not compile time, i.e.:

{
    "someRuntimeKnownWrapperPropertyName" : // The value you actually want
}

Then the easiest way to do that would be to deserialize to a Dictionary<string, T> where the type T corresponds to the type of the expected value, e.g.:

protected override async Task<T> Parse<T>(string uri, string root)
{
    uri = uri + (uri.Contains("?") ? "&" : "?") + "file_type=json";
    using var stream = await Download(uri); // Dispose here or not? What about disposing of the containing HttpResponseMessage?
    
    var options = new JsonSerializerOptions
    {
        AllowTrailingCommas = true,
        // PropertyNameCaseInsensitive = false, Uncomment if you need case insensitivity.
    };
    var dictionary = await JsonSerializer.DeserializeAsync<Dictionary<string, T>>(stream, options);
    // Throw an exception if the dictionary does not have exactly one entry, with the required name
    var pair = dictionary?.Single();
    if (pair == null || !pair.Value.Key.Equals(root, StringComparison.Ordinal)) //StringComparison.OrdinalIgnoreCase if required
        throw new JsonException();
    // And now return the value
    return pair.Value.Value;
}

Notes:

Upvotes: 2

Related Questions