Ignoring properties from controller action model in Swagger using JsonIgnore
Image from Pixabay

Ignoring properties from controller action model in Swagger using JsonIgnore

Excluding properties from Swagger in controller action input model using Newtonsoft.Json package

While designing the REST API, quite often you may find yourself in a situation where the data can be provided from multiple sources, like route, query string or POST/PUT payload. While that might not be a big problem, it may force you to redesign your models and most likely you will end up with keeping your original DTOs but then designing a new model in your application services layer just to have all your data from different sources in one place when you are passing it to the service method.

Resolving multiple sources custom binding

I am going to demonstrate the problem and solution using dummy car catalog web application API project. Imagine following situation where you have to filter models of a specific car brand and you have the following endpoint in your API where data is sent in both route and query data in HTTP GET request:

/brands/{brand}/models?name={name}&year={year}&color={color}

We would definitely like to keep one model which comes as an input to the action method and we just pass it on to the service to give us the collection of car model. This input model/dto would look like this

    public class CarQueryModel
    {
        public String Brand { get; set; }
        public String Name { get; set; }
        public String Color { get; set; }
        public int Year { get; set; }
    }
    

And we already mentioned that we want to keep single model as an input for the controller action, so our contoller action stays with only CarQueryModel input paremeter

    [Route("api/[controller]")]
    [ApiController]
    public class BrandsController : ControllerBase
    {
        readonly ICarModelsService _carModelsService;

        public BrandsController(ICarModelsService carModelsService)
        {
            _carModelsService = carModelsService;
        }

        [HttpGet("{brand}/models")]
        public async Task<ActionResult<IEnumerable<CarViewModel>>> Get([FromQuery] CarQueryModel query)
        {
            return Ok(await _carModelsService.FindModels(query));
        }
    }
    

You can see that this model has property brand which value is supplied from both route and query string. This can be resolved by using custom implementation of IModel binder interface. I covered more on the Mixed model binding in ASP.NET Core using custom model binders in this article. 

For our current case we'll have a simple binder that will bind to our query model from both route and query string.

    public class RouteQueryModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            //Create model instance
            var model = Activator.CreateInstance(bindingContext.ModelMetadata.ModelType);

            //Bind matching values from the 
            foreach (var keyValue in bindingContext.ActionContext.HttpContext.Request.Query)
            {
                SetPropertyValue(model, keyValue.Key, keyValue.Value.FirstOrDefault());
            }

            //Bind matching value from the route
            foreach (var routeValue in bindingContext.ActionContext.RouteData.Values)
            {
                SetPropertyValue(model, routeValue.Key, routeValue.Value);
            }


            //Return the model
            bindingContext.Result = ModelBindingResult.Success(model);
            return Task.CompletedTask;
        }

        private void SetPropertyValue(Object model, String name, Object value)
        {
            var prop = model.GetType().GetProperties().SingleOrDefault(p => p.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase));
            if (prop != null)
            {
                var typedValue = Convert.ChangeType(value, prop.PropertyType);
                prop.SetValue(model, typedValue);
            }
        }

    }
    

Now we just need to tel ASP.NET MVC that we want to apply this binder for this specific model in the controller action, and we do that simply by adding an attribute with type of our custom binder

    [Route("api/[controller]")]
    [ApiController]
    public class BrandsController : ControllerBase
    {
        readonly ICarModelsService _carModelsService;

        public BrandsController(ICarModelsService carModelsService)
        {
            _carModelsService = carModelsService;
        }

        [HttpGet("{brand}/models")]
        public async Task<ActionResult<IEnumerable<CarViewModel>>> Get([FromQuery, ModelBinder(typeof(RouteQueryModelBinder))] CarQueryModel query)
        {
            return Ok(await _carModelsService.FindModels(query));
        }
    }
    

This seems to be working just fine but, we are taking value for the brand property from the route and binding it to our model. Everything is fine, but let's see when we introduce Swagger documentation.

Hiding unnecessary input from the Swagger documentation 

Since we do not want only us to be able to consume our API, we definitely want to document it. The first option when it comes to ASP.NET Core WebApi is definitely Swagger. It auto-generates the proper OpenAPI documentation directly from your code, so you only need to take care that you write proper controllers and optionally document that with comments in code. Swagger will take care of the rest.

I am not going to include Swagger configuration in this article since it is already included in the Github repository of this sample project in Startup.cs class. and if you want to get some more info about REST API documentation in ASP.NET Core with Swagger you can check out Advanced versioning in ASP.NET Core Web API article. It may be a bit outdated, but should be enough to set on the right track. As I mentioned, you can always reference to the sample project public repository from Github.

OK, let see how does Swagger render the UI for our controller action

Swagger Double Prop

Well this does not seem to be right. Although our API will know how to deal with this and to ignore brand value from the query as instructed by our custom binder, it still produces faulty documentation which can be confusing in the first place for the consumer side, which can we front end developer or any third party you are exposing your API to. On the other hand, if the client uses any library for generating clients for your API such as NSwag or AutoRest, they will have also classes produced on their side with two properties.

If you want to know more on how you can automate your API client generation and distribution for .NET Clients using Azure DevOps, checkout Stop writing clients in C# for your Web APIs article.

Even though it does not seem like a breaking issue, it can definitely cause bugs in the process of development and API consuming. Therefore, we need to get rid of the model property, but only from the documentation. In the following case, which value will be taken and which one ignored?

Swagger Double Prop Data 

Now we know that the route value will be taken into consideration, but the consumer of our API who relies on the Swagger documentation does not and that can cause problems. In this particular case wrong brand value will be taken into consideration which is wrong.

Remember, we are using this value from the model in our controller, we just do not want to see it in the documentation as we are going to bind it with our custom binder. This can be done with little help of custom attributes and custom implementation of IOperationFilter interface. Now since there are plenty attributes out there, we can just re-use some of them. I decided to use Newtsoft.Json.JsonIngoreAttribute for this.

Let's first decorate our model DTO class with JsonIgonreAtrribute on the Brand property

    public class CarQueryModel
    {
        [JsonIgnore]
        public String Brand { get; set; }
        public String Name { get; set; }
        public String Color { get; set; }
        public int Year { get; set; }
    }
    

And now to implement our custom document filter class to tell swagger that we do not want to show properties are decorated with JsonIgnoreAttribute 

    public class SwaggerJsonIgnore : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var ignoredProperties = context.MethodInfo.GetParameters()
                .SelectMany(p => p.ParameterType.GetProperties()
                                 .Where(prop => prop.GetCustomAttribute<JsonIgnoreAttribute>() != null)
                                 );
            if (ignoredProperties.Any())
            {
                foreach(var property in ignoredProperties)
                {
                    operation.Parameters = operation.Parameters
                        .Where(p => (!p.Name.Equals(property.Name ,StringComparison.InvariantCulture) && !p.In.Equals("route", StringComparison.InvariantCultureIgnoreCase)))
                        .ToList();
                }

            }
        }
    }
    

If we run our API project and navigate to Swagger, you will see that the property from the UI which we do not require for the binding is gone.

Note

Make sure you include this custom document filter in services registration in Startup.cs. You can refer to a public sample Github repository for this article https://github.com/dejanstojanovic/Sample.CarCatalog.Api/blob/master/Sample.CarCatalog.Api/Startup.cs

Swagger Double Prop Fix

If we execute it, you will see that the URL generated is the correct one and it excludes the duplicated property from the query. To verify that everything is still working as expected from the backend, we'll open quick watch from the Visual Studio debug breakpoint.

Swagger Double Debug

Everything is still working fine and documentation is also generated in the proper way.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article