'System.Text.Json Field Serialization in .NET 5 not shown in Swashbuckle API Definition

Problem

I'm using ASP.NET Core with .NET 5 and am using the System.Text.Json serializer to serialize types containing fields (like System.Numerics.Vector3 (X, Y and Z are fields), although any type with fields behaves the same here).

I've verified that fields get serialized properly by calling the API over Postman, however the Swagger API Definition generated by Swashbuckle does not properly reflect this. (The definition just shows an empty type)

Repro

I've created a gist that reproduces this. It provides an HTTP Get method at /api/Test which returns an object of Type Test with a field and a property. Both are strings. Calling this API via Postman returns the correct values for both. Viewing the Swagger UI at /swagger or the definition at /swagger/v1/swagger.json only shows the property.

This behaviour applies to the examples in the Swagger UI as well, which only include the properties.

Expected behaviour

According to the docs the Swagger Generator should automatically copy the behaviour of System.Text.Json, which is explicitly configured to serialize fields (see line 47), so I'd expect the Swagger definition to include the field.

Summary

To reiterate, I use System.Text.Json to serialize a type with public fields. This works, and I'd prefer keeping it like this.

I try to use Swashbuckle to generate documentation of the API that returns these serializations. This only works for properties, but not fields.

Is there something else that needs to be explicitly configured for this to work?



Solution 1:[1]

It seems like Swashbuckle doesn't use the JsonSerializerOptions to generate the docs. One workaround i found is to handle the types manually:

public class FieldsSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        var fields = context.Type.GetFields();

        if (fields == null) return;
        if (fields.Length == 0) return;

        foreach (var field in fields)
        {
            schema.Properties[field.Name] = new OpenApiSchema
            {
                // this should be mapped to an OpenApiSchema type
                Type = field.FieldType.Name
            };
        }
    }
}

Then in your Startup.cs ConfigureServices:

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication1", Version = "v1" });
    c.SchemaFilter<FieldsSchemaFilter>();
});

When stepping through, you'll see the JsonSerializerOptions used in the SchemaFilterContext (SchemaGenerator). IncludeFields is set to true. Still only properties are used for docs, so I guess a filter like that is your best bet.

Solution 2:[2]

The issue has no thing to do with Swagger, it is pure serialization issue.

You have 3 solutions:

  1. Write your own customized json for vector. (just concept)
  2. Use a customized object with primitive types and map it. (just concept)
  3. Use Newtonsoft.Json (suggested solution)

Regarding to Microsoft doc, System.Text.Json you can see in the comparing list, that System.Text.Json might have some limitation.

If you want the suggested solution jump directly to solution 3.

Let's take the first concept of custom serialized. Btw this custom example is just for demonstration and not full solution.

So what you can do is following:

  1. Create a custom vector CustomVector model.
  2. Create a custom VectorConverter class that extend JsonConverter.
  3. Added some mapping.
  4. Put the attribute VectorConverter to vector property.

Here is my attempt CustomVector:

public class CustomVector
{
    public float? X { get; set; }
    public float? Y { get; set; }
    public float? Z { get; set; }
}

And custom VectorConverter:

public class VectorConverter : JsonConverter<Vector3>
{
    public override Vector3 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        // just for now
        return new Vector3();
    }

    public override void Write(Utf8JsonWriter writer, Vector3 data, JsonSerializerOptions options)
    {
        // just for now
        var customVector = new CustomVector
        {
            X = data.X,
            Y = data.Y,
            Z = data.Z
        };

        var result = JsonSerializer.Serialize(customVector);

        writer.WriteStringValue(result);
    }
}

And you vector property, added the following attribute:

[JsonConverter(typeof(VectorConverter))]
public Vector3 Vector { get; set; }

This will return following result:

enter image description here

Now this solve part of the issue, if you want to post a vector object, you will have another challenge, that also depends on your implementation logic.

Therefore, comes my second solution attempt where we expose our custom vector and ignore vector3 in json and map it to/from Vector3 from our code:

So hence we have introduces a CustomVector, we can use that in stead of Vector3 in our model, than map it to our Vector3.

public class Test
{
    public string Field { get; set; }
    public string Property { get; set; }
    [JsonIgnore]
    public Vector3 Vector { get; set; }
    public CustomVector CustomVector { get; set; }
}

enter image description here

Here is a get and post method with mapping example:

[HttpGet]
public Test Get()
{
    var vector = new CustomVector() { X = 1, Y = 1, Z = 1 };
    var test = new Test
    {
        Field = "Field",
        Property = "Property",
        CustomVector = vector
    };
    VectorMapping(test);
    return test;
}

[HttpPost]
public Test Post(Test test)
{
    VectorMapping(test);
    return test;
}

private static void VectorMapping(Test test)
{
    test.Vector = new Vector3
    {
        X = test.CustomVector.X.GetValueOrDefault(),
        Y = test.CustomVector.Y.GetValueOrDefault(),
        Z = test.CustomVector.Z.GetValueOrDefault()
    };
}

The down side in first solution, we need to write a full customize serializing, and in our second solution we have introduced extra model and mapping.

The suggested solution

Therefore I suggest the following and 3rd attempt:

Keep every thing you have as it is in your solution, just added nuget Swashbuckle.AspNetCore.Newtonsoft to your project, like:

<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="5.6.3" />

And in your startup

services.AddSwaggerGenNewtonsoftSupport();

Fire up, and this will generate the documentation, as it allow serializing and deserializing Vector3 and other class types that are not supported by System.Text.Json.

As you can see this include now Vector3 in documentation:

enter image description here

I am pretty sure this can be done other ways. So this is my attempts solving it.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2