Search code examples
c#azure-functionsjson.netswaggerswashbuckle

Azure Function: OpenApi/swagger ignores System.Text.Json.Serialization.JsonPropertyName attribute


I have a simple azure function triggered by http:

    [OpenApiOperation(operationId: "GetX", tags: new[] { nameof(GetXHttpTrigger) })]
    [OpenApiParameter(name: "id", In = ParameterLocation.Path, Required = true, Type = typeof(Guid), Description = "X id.")]
    [OpenApiResponseWithBody(statusCode: HttpStatusCode.OK, contentType: "application/json", bodyType: typeof(X), Description = "The OK response")]
    [OpenApiResponseWithBody(statusCode: HttpStatusCode.NotFound, contentType: "application/json", bodyType: typeof(StatusMessage), Description = "The NotFound response")]
    [Function(nameof(GetXHttpTrigger))]
    public async Task<HttpResponseData> Run(
        [HttpTrigger(
            AuthorizationLevel.Function,
            "get",
            Route = "Xs/{id}")]
        HttpRequestData req,
        Guid id,
        CancellationToken cancellationToken)

where X is:

    public class X
    {
        [System.Text.Json.Serialization.JsonPropertyName("combined_value")]
        public string CombinedValue { get; set; }
    ..

The code that returns data to the caller looks like:

        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync<X>(xData); // WriteAsJson is extension from Microsoft.Azure.Functions.Worker.Http

The data returned to the caller is correct:

    {
        "combined_value" : "1"
    }

but the openId yaml generated schema (from http://localhost:port/api/openapi/{version}.{extension}) ignores value configured in the attribute and generates just:

    x:
        type: object
        properties:
        combinedValue:
          type: string

pay attention that combinedValue is "lowerCased" value from c# code. Any ideas why it might happen and how to force swagger to consider System.Text.Json.Serialization.JsonPropertyName attribute?

My host configuration is quite simple:

    var host = Host.CreateDefaultBuilder()
        .ConfigureFunctionsWorkerDefaults((context, builder) =>
        {
            // do nothing
        })
        .ConfigureOpenApi()

and this is what packages I have:

    <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.21.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.19.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.0.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.0.13" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.15.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.OpenApi" Version="1.5.1" />

plus some more project configuration:

<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>

UPDATE: If I configure Newtonsoft.Json.JsonPropertyAttribute for c# class:

    [Newtonsoft.Json.JsonProperty("combined_value")]
    public string CombinedValue { get; set; }

The generated output will be correct:

x:
    type: object
    properties:
    combined_value:
      type: string

But I don't want to mix these 2 approaches, how to force swagger using System.Text.Json.Serialization in this case?


Solution

  • Microsoft.Azure.Functions.Worker.Extensions.OpenApi nuget has dependency on Newtonsoft.Json and according to docs it does not support System.Text.Json. You can switch completely to using Newtonsoft.Json, as far as I udnerstand it can be done with the following (have not tried it):

    .ConfigureFunctionsWorkerDefaults(worker =>
      {
        worker.UseNewtonsoftJson();
      })
    

    Then Newtonsoft.Json.JsonProperty will be used for both serialization and OpenAPI generation.

    Alternatively you can either use both types of attributes:

    [System.Text.Json.Serialization.JsonPropertyName("combined_value")]
    [Newtonsoft.Json.JsonProperty("combined_value")]
    public string CombinedValue { get; set; }
    

    Or add custom contract resolver so it will use attributes from one library for another (like is done here for System.Text.Json -> Json.NET, possibly will require configuring JsonConvert.DefaultSettings, also similar can be done vice versa).

    Also see System.Text.Json Support issue @github.