Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Make names of named tuples appear in serialized JSON responses

Situation: I have multiple Web service API calls that deliver object structures. Currently, I declare explicit types to bind those object structures together. For the sake of simplicity, here's an example:

[HttpGet] [ProducesResponseType(typeof(MyType), 200)] public MyType TestOriginal() {     return new MyType { Speed: 5.0, Distance: 4 }; } 

Improvement: I have loads of these custom classes like MyType and would love to use a generic container instead. I came across named tuples and can successfully use them in my controller methods like this:

[HttpGet] [ProducesResponseType(typeof((double speed, int distance)), 200)] public (double speed, int distance) Test() {     return (speed: 5.0, distance: 4); } 

Problem I am facing is that the resolved type is based on the underlying Tuple which contains these meaningless properties Item1, Item2 etc. Example:

enter image description here

Question: Has anyone found a solution to get the names of the named tuples serialized into my JSON responses? Alternatively, has anyone found a generic solution that allows to have a single class/representation for random structures that can be used so that the JSON response explicitly names what it contains.

like image 654
Quality Catalyst Avatar asked Aug 29 '17 06:08

Quality Catalyst


People also ask

Are tuples JSON serializable?

Python tuples are JSON serializable, just like lists or dictionaries. The JSONEncoder class supports the following objects and types by default. The process of converting a tuple (or any other native Python object) to a JSON string is called serialization.

Are tuples serializable?

The results shows Tuple<T1,T2,T3,...> is serializable and deserializable.

Can JSON serialize a list?

To serialize a collection - a generic list, array, dictionary, or your own custom collection - simply call the serializer with the object you want to get JSON for. Json.NET will serialize the collection and all of the values it contains.


2 Answers

For serializing response just use any custom attribute on action and custom contract resolver (this is only solution, unfortunately, but I'm still looking for any more elegance one).

Attribute:

public class ReturnValueTupleAttribute : ActionFilterAttribute {     public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)     {         var content = actionExecutedContext?.Response?.Content as ObjectContent;         if (!(content?.Formatter is JsonMediaTypeFormatter))         {             return;         }          var names = actionExecutedContext             .ActionContext             .ControllerContext             .ControllerDescriptor             .ControllerType             .GetMethod(actionExecutedContext.ActionContext.ActionDescriptor.ActionName)             ?.ReturnParameter             ?.GetCustomAttribute<TupleElementNamesAttribute>()             ?.TransformNames;          var formatter = new JsonMediaTypeFormatter         {             SerializerSettings =             {                 ContractResolver = new ValueTuplesContractResolver(names),             },         };          actionExecutedContext.Response.Content = new ObjectContent(content.ObjectType, content.Value, formatter);     } } 

ContractResolver:

public class ValueTuplesContractResolver : CamelCasePropertyNamesContractResolver {     private IList<string> _names;      public ValueTuplesContractResolver(IList<string> names)     {         _names = names;     }      protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)     {         var properties = base.CreateProperties(type, memberSerialization);         if (type.Name.Contains(nameof(ValueTuple)))         {             for (var i = 0; i < properties.Count; i++)             {                 properties[i].PropertyName = _names[i];             }              _names = _names.Skip(properties.Count).ToList();         }          return properties;     } } 

Usage:

[ReturnValueTuple] [HttpGet] [Route("types")] public IEnumerable<(int id, string name)> GetDocumentTypes() {     return ServiceContainer.Db         .DocumentTypes         .AsEnumerable()         .Select(dt => (dt.Id, dt.Name)); } 

This one returns next JSON:

[      {         "id":0,       "name":"Other"    },    {         "id":1,       "name":"Shipping Document"    } ] 

Here the solution for Swagger UI:

public class SwaggerValueTupleFilter : IOperationFilter {     public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)     {         var action = apiDescription.ActionDescriptor;         var controller = action.ControllerDescriptor.ControllerType;         var method = controller.GetMethod(action.ActionName);         var names = method?.ReturnParameter?.GetCustomAttribute<TupleElementNamesAttribute>()?.TransformNames;         if (names == null)         {             return;         }          var responseType = apiDescription.ResponseDescription.DeclaredType;         FieldInfo[] tupleFields;         var props = new Dictionary<string, string>();         var isEnumer = responseType.GetInterface(nameof(IEnumerable)) != null;         if (isEnumer)         {             tupleFields = responseType                 .GetGenericArguments()[0]                 .GetFields();         }         else         {             tupleFields = responseType.GetFields();         }          for (var i = 0; i < tupleFields.Length; i++)         {             props.Add(names[i], tupleFields[i].FieldType.GetFriendlyName());         }          object result;         if (isEnumer)         {             result = new List<Dictionary<string, string>>             {                 props,             };         }         else         {             result = props;         }          operation.responses.Clear();         operation.responses.Add("200", new Response         {             description = "OK",             schema = new Schema             {                 example = result,             },         });     } 
like image 158
anatol Avatar answered Oct 22 '22 16:10

anatol


You have a little bid conflicting requirements

Question:

I have loads of these custom classes like MyType and would love to use a generic container instead

Comment:

However, what type would I have to declare in my ProducesResponseType attribute to explicitly expose what I am returning

Based on above - you should stay with types you already have. Those types provide valuable documentation in your code for other developers/reader or for yourself after few months.

From point of readability

[ProducesResponseType(typeof(Trip), 200)] 

will be better then

[ProducesResponseType(typeof((double speed, int distance)), 200)] 

From point of maintainability
Adding/removing property need to be done only in one place. Where with generic approach you will need to remember update attributes too.

like image 38
Fabio Avatar answered Oct 22 '22 14:10

Fabio