Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swagger schema reports all unit properties and not UnitValue ones #1256

Open
danielecapursi opened this issue Jun 8, 2023 · 4 comments
Open
Labels

Comments

@danielecapursi
Copy link

danielecapursi commented Jun 8, 2023

Describe the bug
When using UnitsNetIQuantityJsonConverter (and hence Newtonsoft.JSon serializer), Swagger generates the schema of all units (e.g. Length) as they are as C# classes, reporting all their properties (value, unit, feetInches, quantityInfo, dimensions and all related subobjects).
Bug UnitsNetIQuantityJsonConverter convert values such that they are (de)serialized as ValueUnit, i.e. having value and unit properties only.

To Reproduce
Steps to reproduce the behavior:
Use this solution: WebTest.zip

It exposes a single method as a Rest API (/Api), returning a MyDto object, having Location = 12.345 Km.
Try calling /Api.
The result will correctly be:

{
  "location": {
    "unit": "LengthUnit.Kilometer",
    "value": 12.345
  }
}

But MyDto will have Example Values:

{
  "location": {
    "feetInches": {
      "feet": 0,
      "inches": 0
    },
    "value": 0,
    "unit": 1,
    "quantityInfo": {
      "name": "string",
      "unitInfos": [
        {
          "value": 1,
          "pluralName": "string",
          "baseUnits": {
            "length": 1,
            "mass": 1,
            "time": 1,
            "current": 1,
            "temperature": 1,
...
}

I truncated it a lot, since it is veeeeeery long (more than 360 lines).
Please note that this is not coherent with the JSON actually returned by the API. This is the bug!

And MyDto will have schema:

MyDto{
location
	Length{
		feetInches	FeetInches{...}
		value	[...]
		unit	LengthUnit[...]
		quantityInfo	LengthUnitQuantityInfo{...}
		dimensions	BaseDimensions{...}
		}
}

Expected behavior
Example Values should be:

{
  "location": {
    "value": 0,
    "unit": "string"
  }
}

Schema should be:

MyDto{
	location	
		Length
		{
			value	number($double)
			unit	string
		}
}

Additional context
The problem is very easy to spot and seems easy to fix: we are using a converter, but Swagger has no idea of it. We just need to inform Swagger about the substitution of all IQuantityValue with ValueUnit (ExtendedValueUnit, in some cases).

@danielecapursi
Copy link
Author

I fixed it (to some extents) using an ISchemaFilter:

In Program.cs:

builder.Services.AddSwaggerGen(c =>
{
    c.SchemaFilter<QuantitySerializationFilter>();
});

The ISchemaFilter is defined in this way:

   public class QuantitySerializationFilter : ISchemaFilter
   {
       public void Apply(OpenApiSchema schema, SchemaFilterContext context)
       {
           if (typeof(IQuantity).IsAssignableFrom(context.Type))
           {
               schema.Properties = schema.Properties.Where(p => p.Key == "value" || p.Key == "unit").ToDictionary(k => k.Key, k => k.Value);
               schema.Properties["unit"].Type = "string";
               schema.Properties["unit"].Reference = null;
           }
       }
   }

It works: now I have the right Example Values and Schema.
But I don't think this is a complete fix, since:

  1. Swagger keeps producing the schemas of AmountOfSubstanceUnit, Assembly, BaseDimensions, etc.
    image
  2. The above code has the properties value and unit hardwired, while they should be taken dynamically from the UnitValue type.

@angularsen
Copy link
Owner

angularsen commented Jun 10, 2023

Thanks for sharing. I don't have any knowledge on this specific problem with Swagger, but I would recommend you to consider creating your own data transfer DTO types for representing quantities. Then you are in full control of its serialization, and you can remove all the cruft that exists in these types, since you typically just need Value and Unit properties. Just create a simple mapping function to/from QuantityDto and IQuantity.

The DTO could be as simple as:

public record QuantityDto(double Value, string QuantityType, string Unit); 

// Example: 
QuantityDto dto = new(Value: 5.0, QuantityType: "Length", Unit: "Centimeter");

Then you don't need any special serializers injected here and there to make things work.

Also, if we incur a breaking change in our JSON serializer, which we do really try to avoid, then your API gets a breaking change.

@angularsen
Copy link
Owner

I realize it was not obvious how to accomplish this, so I created some helper methods Quantity.From() and .TryFrom() that can construct quantities from strings, and updated the wiki with an example.

New methods added:
#1258

Wiki:
https://github.com/angularsen/UnitsNet/wiki/Serializing-to-JSON,-XML-and-more#-recommended-map-to-your-own-custom-dto-types

@angularsen
Copy link
Owner

Nuget is out shortly, with the new methods.

Release UnitsNet/5.19.0 · angularsen/UnitsNet

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants