16

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?

Maytham Fahmi
  • 31,138
  • 14
  • 118
  • 137
blenderfreaky
  • 738
  • 7
  • 26

2 Answers2

8

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.

Markus Dresch
  • 5,290
  • 3
  • 20
  • 40
3

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.

Maytham Fahmi
  • 31,138
  • 14
  • 118
  • 137
  • 1
    The fields are already serialized correctly (.NET 5 supports field serialization, although you need to enable it first), the issue is that swagger does not properly generate documentation for that. – blenderfreaky Dec 07 '20 at 11:02
  • Thank you for your feedback. I think there are some unclear stuff in your question you mentioning vector3 issue and it is not in your gist, so I was just improvising the issue, may be if you could update the gist and show what you excepting the documentation to be look like. I want to see that so I can update my answer. – Maytham Fahmi Dec 07 '20 at 11:33
  • @blenderfreaky I have added a 3rd and suggested solution, that added documentation correctly hope it helps. – Maytham Fahmi Dec 08 '20 at 23:52
  • I don't see why Newtonsoft would be required, `Vector3` and other types containing fields *are* supported by `System.Text.Json` (it's explicitly enabled in the gist, see [line 47](https://gist.github.com/blenderfreaky/edfc56b1261d54d4c63e8e2156534f54#file-swashbucklerepro-cs-L47)). The docs you mention also specify that [fields are supported](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-5-0#public-and-non-public-fields) – blenderfreaky Dec 09 '20 at 07:24
  • @blenderfreaky I do not disagree with you, there might be still a bug in it or some thing else that is out of the scope of providing why system.text.json has the issue. I am also using system.text.json in many cases, in some situation where we have inheritance objects we get also ugly issues with out any explanation. I will still say the last option is preferable IMO. – Maytham Fahmi Dec 09 '20 at 09:46