I have a .NET Core API that is documented via Swashbuckle and Swagger. The "example" that is generated for the UI doesn't seem to be including nested objects in the request properly, although they are handled and processed correctly when executing the endpoints.
I have a CreatePaymentRequest class, which is received from the body of the HTTP request, which contains a property of a Notional type. Notional is made up of a decimal value, and a string value.
The generated example looks like this:
{
"tradeId": 0,
"settlementMeans": "SWIFT",
"notional1": {},
"notional1Rate": 0,
"notional2": {},
"notional2Rate": 0,
"paymentReference": "string",
"description": "string"
}
When I would expect it to look like this:
{
"tradeId": 0,
"settlementMeans": "SWIFT",
"notional1": {"Amount": 0, "Currency": "string"},
"notional1Rate": 0,
"notional2": {"Amount": 0, "Currency": "string"},
"notional2Rate": 0,
"paymentReference": "string",
"description": "string"
}
The generated schema also seems to be off, as it doesn't include the properties for Notional:
Though it is listed properly in the "Schemas" section at the bottom:
Here are the classes that make up each object:
public class Request
{
public long TradeId { get; set; }
public SettlementMeans SettlementMeans { get; set; }
public Notional Notional1 { get; set; }
public decimal Notional1Rate { get; set; }
public Notional Notional2 { get; set; }
public decimal Notional2Rate { get; set; }
public string PaymentReference { get; set; } = "";
public string Description { get; set; } = "";
}
public class Notional
{
[JsonContructor]
public Notional(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public Notional()
{
Amount = 0;
Currency = "XXX";
}
public decimal Amount { get; }
public string Currency { get; }
}
Controller method:
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CreatePaymentApi_Response))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(BadRequestResponse))]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<ActionResult<CreatePaymentApi_Response>> PostCreate(CreatePaymentApi_Request request)
{
var command = _mapper.Map<CreatePaymentHandler.Request>(request);
var response = await _mediator.Send(command);
var result = _mapper.Map<CreatePaymentHandler.Response>(response);
return Ok(result);
}
I am setting up SwaggerGen using services.AddSwaggerGen(); in ConfigureServices without any special options.
Having to constantly type out, or copy / paste the JSON for the Notional values is starting to become very repetitive when debugging via the Swashbuckle UI. Is there anything I can do to get the example to generate as I expect it to?
I was eventually able to track this down to the readonly Amount and Currency properties on Notional. I was expecting it to use the constructor marked with JsonConstructor, but apparently, this is only supported when using the newtonsoft serializer, NOT System.Text.Json.
In theory, I should be able to work around this using a custom ISchemaFilter, but I've decided that it isn't worth it just yet to implement one.
Related
Hey all (newbie here) I am developing A Xamarin forms application In which I am already making API requests (Code is Down Below), However, the response I am getting back at the moment looks like this:
{
"result": "success",
"documentation": "https://www.exchangerate-api.com/docs",
"terms_of_use": "https://www.exchangerate-api.com/terms",
"time_last_update_unix": 1673308801,
"time_last_update_utc": "Tue, 10 Jan 2023 00:00:01 +0000",
"time_next_update_unix": 1673395201,
"time_next_update_utc": "Wed, 11 Jan 2023 00:00:01 +0000",
"base_code": "EUR",
"target_code": "GBP",
"conversion_rate": 0.8803
}
I am Only using the conversion rate variable however in the next API I am hoping to use all this variables are stored in parameters (Class I guess?) called Data, so currently, the class I am using to store the variable which is grabbed from this API response looks like this:
public double conversion_rate { get; set; }
So how would I adapt this code to interpret that data, the response is below (I want the data labeled "actual" in the "intensity" section TIA):
"data": [
{
"from": "2023-01-10T19:30Z",
"to": "2023-01-10T20:00Z",
"intensity": {
"forecast": 70,
"actual": 79,
"index": "low"
}
}
]
Ive attempted to find a solution on my own for a while, looked all around and still nothing to see :)
I would create two DTO (data transfer object) classes which contain the properties you're interested in. Once you make the API request, you can map or parse the relevant fields to another object or type.
FYI - you should rename conversion_rate to ConversionRate, which is the standard for property names in C# and then add [JsonPropertyName("conversion_rate")] above the property.
First, you can write the c# class entity based on the json file. Here is the demo and you can refer to.
public class Root
{
public string result { get; set; }
public string documentation { get; set; }
public string terms_of_use { get; set; }
public int time_last_update_unix { get; set; }
public string time_last_update_utc { get; set; }
public int time_next_update_unix { get; set; }
public string time_next_update_utc { get; set; }
public string base_code { get; set; }
public string target_code { get; set; }
public double conversion_rate { get; set; }
}
Then, you can write the method to deserialize the json file.
using System.Text.Json;
var options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var codeRoot = JsonSerializer.Deserialize<Root>(Json, options);
I have the following response structure which will be serialized following an API call.
public class Details
{
public string firstName { get; set; }
}
public class Result
{
public int maxTry { get; set; }
public int minTry { get; set; }
public List<Details> aggr { get; set; }
public List<string> features { get; set; }
public string zone { get; set; }
}
[Serializable]
public class Root
{
public List<Result> result { get; set; }
}
I have the following array of objects which is an API response.
"result": [
{
“maxTry: 17,
"minTry”: 10,
"details": [
{
“firstName”: “Sam”,
},
{
"firstName”: ”Julio”,
}
],
"aggr": [
“Abc”,
],
"zone": “D3”
},
{
"aggr": [
"Abc",
],
"zone": “C3”
},
{
"aggr": [
"Abc",
],
"zone": “B2”
},
]
}
The problem is even though some objects does not have maxTry and minTry property in the response, when I deserialize the response as follows,
var jobject = JsonConvert.DeserializeObject<Root>(res);
I am still getting the output with maxTry and minTry defaulted to 0. Can this be instead defaulted to null
Sure, make them nullable ints by putting a question mark after int
[JsonProperty("maxTry")] //newtonsoft
public int? MaxTry { get; set; }
[JsonProperty("minTry")]
public int? MinTry { get; set; }
The JsonProperty attribute let's you name your c# property differently to the json, so you can keep c# naming conventions regardless what the json looks like. If you use System.Text.Json it's JsonPropertyName instead
The reason why your ints are coming out as 0 is that ints are value types, which essentially means they must have a value and cannot be null. The default value for an int is 0; even if you never set a value for MaxTry it would have a value of 0
var r = new Result();
Console.WriteLine(r.MaxTry); //prints 0
Nullable ints are harder to work with, because they might be null; you'll need to check if(r.MaxTry.HasValue) if you want to see if it's null or not before you do some things with them. To some degree you might want to reconsider leaving them as ints and treating 0 as them having been absent. It might work quite well for your context for example:
for(int try = 0; try < result.MaxTry; try++)
This won't run if MaxTry is 0.. if that's a problem then you need to decide if maxtry being 0 is ever valid and if it's not, provide some other sensible default if it is 0
Change those properties to nullable int
public int? maxTry { get; set; }
public int? minTry { get; set; }
I already looked at a lot of other questions with the same problem but never found a definitive solution that actually works for me. I tried using the JsonExtensionData Attribute, that doesn't work though since I can't convert my other data class to an object and it throws the Invalid extension data attribute on 'NAMESPACE'. Member 'Sols' type must implement IDictionary<string, JToken>. error.
My current data model class looks like this
public partial class Mars
{
public Dictionary<string, Sol> Sols { get; set; }
[JsonProperty("sol_keys")]
public List<long> SolKeys { get; set; }
}
public partial class Sol
{
[JsonProperty("AT")]
public At At { get; set; }
[JsonProperty("First_UTC")]
public DateTimeOffset FirstUtc { get; set; }
[JsonProperty("Last_UTC")]
public DateTimeOffset LastUtc { get; set; }
[JsonProperty("Season")]
public string Season { get; set; }
}
public partial class At
{
[JsonProperty("av")]
public double Av { get; set; }
[JsonProperty("ct")]
public long Ct { get; set; }
[JsonProperty("mn")]
public double Mn { get; set; }
[JsonProperty("mx")]
public double Mx { get; set; }
}
The json data looks like this
{
"651":
{
"AT":
{
"av": -61.957,
"ct": 302204,
"mn": -96.733,
"mx": -15.877
},
"First_UTC": "2020-09-25T02:42:14Z",
"Last_UTC": "2020-09-26T03:21:49Z",
"Season": "summer"
},
"652": {
"AT": {
"av": -65.002,
"ct": 278608,
"mn": -96.111,
"mx": -15.653
},
"First_UTC": "2020-09-26T03:21:50Z",
"Last_UTC": "2020-09-27T04:01:24Z",
"Season": "summer"
},
"sol_keys": [
"646",
"647",
"648",
"649",
"650",
"651",
"652"
]
}
I can't really modify the json data since I get it from an api.
I basically just want to select one of the numbers and then get the Sol data of that object.
Any help would be appreciated.
The JSON doesn't fit well with the C# type system. However, you can still use Json.Net to parse it. You just need to introduce some extra steps.
First step is to parse the JSON to a JObject:
var jObject = JsonConvert.DeserializeObject<JObject>(json);
Then you can extract the sol_keys:
var solKeys = jObject.GetValue("sol_keys").ToObject<long[]>();
Now it becomes a bit tricky. If you remove the sol_keys from the JSON (in this case the parsed JSON) it has the structure of a dictionary of Sol objects that you are able to parse:
jObject.Remove("sol_keys");
var mars = jObject.ToObject<Dictionary<long, Sol>>();
Now you have both solKeys and mars parsed from the JSON. Furthermore the solKeys and the keys in the dictionary share the same type (long).
We've got an API, which simply posts incoming JSON documents to a message bus, having assigned a GUID to each. We're upgrading from .Net Core 2.2 to 3.1 and were aiming to replace NewtonSoft with the new System.Text.Json library.
We deserialise the incoming document, assign the GUID to one of the fields and then reserialise before sending to the message bus. Unfortunately, the reserialisation is failing with the exception Operation is not valid due to the current state of the object.
Here's a controller that shows the problem:-
using System;
using System.Net;
using Project.Models;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Text;
using System.Text.Json;
namespace Project.Controllers
{
[Route("api/test")]
public class TestController : Controller
{
private const string JSONAPIMIMETYPE = "application/vnd.api+json";
public TestController()
{
}
[HttpPost("{eventType}")]
public async System.Threading.Tasks.Task<IActionResult> ProcessEventAsync([FromRoute] string eventType)
{
try
{
JsonApiMessage payload;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) {
string payloadString = await reader.ReadToEndAsync();
try {
payload = JsonSerializer.Deserialize<JsonApiMessage>(payloadString);
}
catch (Exception ex) {
return StatusCode((int)HttpStatusCode.BadRequest);
}
}
if ( ! Request.ContentType.Contains(JSONAPIMIMETYPE) )
{
return StatusCode((int)HttpStatusCode.UnsupportedMediaType);
}
Guid messageID = Guid.NewGuid();
payload.Data.Id = messageID.ToString();
// we would send the message here but for this test, just reserialise it
string reserialisedPayload = JsonSerializer.Serialize(payload);
Request.HttpContext.Response.ContentType = JSONAPIMIMETYPE;
return Accepted(payload);
}
catch (Exception ex)
{
return StatusCode((int)HttpStatusCode.InternalServerError);
}
}
}
}
The JsonApiMessage object is defined like this:-
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Project.Models
{
public class JsonApiMessage
{
[JsonPropertyName("data")]
public JsonApiData Data { get; set; }
[JsonPropertyName("included")]
public JsonApiData[] Included { get; set; }
}
public class JsonApiData
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("attributes")]
public JsonElement Attributes { get; set; }
[JsonPropertyName("meta")]
public JsonElement Meta { get; set; }
[JsonPropertyName("relationships")]
public JsonElement Relationships { get; set; }
}
}
An example call looks like this:-
POST http://localhost:5000/api/test/event
Content-Type: application/vnd.api+json; charset=UTF-8
{
"data": {
"type": "test",
"attributes": {
"source": "postman",
"instance": "jg",
"level": "INFO",
"message": "If this comes back with an ID, the API is probably working"
}
}
}
When I examine the contents of payload at a breakpoint in Visual Studio, it looks OK at the top level but the JsonElement bits look opaque, so I don't know if they've been parsed properly. Their structure can vary, so we only care that they are valid JSON. In the old NewtonSoft version, they were JObjects.
After the GUID has been added, it appears in the payload object when examined at a breakpoint but I'm suspicious that the problem is related to other elements in the object being read-only or something similar.
Your problem can be reproduced with the following more minimal example. Define the following model:
public class JsonApiMessage
{
public JsonElement data { get; set; }
}
Then attempt to deserialize and re-serialize an empty JSON object like so:
var payload = JsonSerializer.Deserialize<JsonApiMessage>("{}");
var newJson = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
And you will get an exception (demo fiddle #1 here):
System.InvalidOperationException: Operation is not valid due to the current state of the object.
at System.Text.Json.JsonElement.WriteTo(Utf8JsonWriter writer)
at System.Text.Json.Serialization.Converters.JsonConverterJsonElement.Write(Utf8JsonWriter writer, JsonElement value, JsonSerializerOptions options)
The problem seems to be that JsonElement is a struct, and the default value for this struct can't be serialized. In fact, simply doing JsonSerializer.Serialize(new JsonElement()); throws the same exception (demo fiddle #2 here). (This contrasts with JObject which is a reference type whose default value is, of course, null.)
So, what are your options? You could make all your JsonElement properties be nullable, and set IgnoreNullValues = true while re-serializing:
public class JsonApiData
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("attributes")]
public JsonElement? Attributes { get; set; }
[JsonPropertyName("meta")]
public JsonElement? Meta { get; set; }
[JsonPropertyName("relationships")]
public JsonElement? Relationships { get; set; }
}
And then:
var reserialisedPayload = JsonSerializer.Serialize(payload, new JsonSerializerOptions { IgnoreNullValues = true });
Demo fiddle #3 here.
Or, in .NET 5 or later, you could mark all of your JsonElement properties with [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]:
public class JsonApiData
{
// Remainder unchanged
[JsonPropertyName("attributes")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonElement Attributes { get; set; }
[JsonPropertyName("meta")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonElement Meta { get; set; }
[JsonPropertyName("relationships")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public JsonElement Relationships { get; set; }
}
Doing so will cause uninitialized elements to be skipped during serialization without needing to modify serialization options.
Demo fiddle #4 here.
Or, you could simplify your data model by binding all the JSON properties other than Id to a JsonExtensionData property like so:
public class JsonApiData
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
}
This approach avoids the need to manually set IgnoreNullValues when re-serializing, and thus ASP.NET Core will re-serialize the model correctly automatically.
Demo fiddle #5 here.
The exception is right - the state of the object is invalid. The Meta and Relasionships elements are non-nullable but the JSON string doesn't contain them. The deserialized object ends up with Undefined values in those properties that can't be serialized.
[JsonPropertyName("meta")]
public JsonElement? Meta { get; set; }
[JsonPropertyName("relationships")]
public JsonElement? Relationships { get; set; }
The quick fix would be to change those properties to JsonElement?. This will allow correct deserialization and serialization. By default, the missing elements will be emitted as nulls:
"meta": null,
"relationships": null
To ignore them, add the IgnoreNullValues =true option :
var newJson = JsonSerializer.Serialize(payload, new JsonSerializerOptions
{ WriteIndented = true,IgnoreNullValues =true });
The real solution though would be to get rid of all that code. It hampers the use of System.Text.Json. Left by itself, ASP.NET Core uses Pipelines to read the input stream without allocating, deserializes the payload and calls the method with the deserialized object as a parameter, using minimal allocations. Any return values are serialized in the same way.
The question's code though allocates a lot - it caches the input in the StreamReader, then the entire payload is cached in the payloadString and then again, as the payload object. The reverse process also uses temporary strings. This code takes at least twice as much RAM as ASP.NET Core would use.
The action code should be just :
[HttpPost("{eventType}")]
public async Task<IActionResult> ProcessEventAsync([FromRoute] string eventType,
MyApiData payload)
{
Guid messageID = Guid.NewGuid();
payload.Data.Id = messageID.ToString();
return Accepted(payload);
}
Where MyApiData is a strongly-typed object. The shape of the Json example corresponds to :
public class Attributes
{
public string source { get; set; }
public string instance { get; set; }
public string level { get; set; }
public string message { get; set; }
}
public class Data
{
public string type { get; set; }
public Attributes attributes { get; set; }
}
public class MyApiData
{
public Data data { get; set; }
public Data[] included {get;set;}
}
All other checks are performed by ASP.NET Core itself - ASP.NET Core will reject any POST that doesn't have the correct MIME type. It will return a 400 if the request is badly formatted. It will return a 500 if the code throws
Using a .Net Core 1.0 Lambda I want to be able to create a Lambda function which handles the PreSignUp trigger from an AWS Cognito User pool.
using Amazon.Lambda.Core;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]
public class PreSignUp_SignUp
{
public string userPoolId { get; set; }
public const string EmailKey = "email";
public const string PhoneNumber = "phone_number";
public Dictionary<string,string> userAttributes { get; set; }
public Dictionary<string, string> validationData { get; set; }
}
public class PreSignup_SignUpResponse
{
public bool autoConfirmUser { get; set; }
}
public class Function
{
public PreSignup_SignUpResponse FunctionHandler(PreSignUp_SignUp input, ILambdaContext context)
{
return new PreSignup_SignUpResponse { autoConfirmUser = true };
}
}
Though the request succeeds and returns a response when invoking the Lambda with an example request of:
{
"datasetName": "datasetName",
"eventType": "SyncTrigger",
"region": "us-east-1",
"identityId": "identityId",
"datasetRecords": {
"SampleKey2": {
"newValue": "newValue2",
"oldValue": "oldValue2",
"op": "replace"
},
"SampleKey1": {
"newValue": "newValue1",
"oldValue": "oldValue1",
"op": "replace"
}
},
"identityPoolId": "identityPoolId",
"version": 2
}
When performing an actual SignUp via the .Net AmazonCognitoIdentityProviderClient I get back an error:
Amazon.CognitoIdentityProvider.Model.InvalidLambdaResponseException :
Unrecognizable lambda output
Which I'm guessing means I have not got the shape of the response (and possibly even request) correct.
Does anyone have an example of a .Net Lambda function that works for the PreSignUp trigger in AWS Cognito?
The cognito trigger requests/responses must contain the entire payload as specified in the Cognito trigger documentation:
http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html
I have found while diagnosing this issue the best place to start is by creating a function handler that takes a JObject and then logs and return's that same object e.g.
public JObject FunctionHandler(JObject input, ILambdaContext context)
{
context.Logger.LogLine("Input was: " + input);
return input;
}
This captures the payload in cloudwatch logs and then helps steer you towards the strongly typed structured required.
In my case for PreSignUp I ended up creating the following types to make a simple function which auto-verifies all supplied credentials.
public abstract class AbstractTriggerRequest
{
[JsonProperty("userAttributes")]
public Dictionary<string, string> UserAttributes { get; set; }
}
public abstract class AbstractTriggerResponse
{
}
public class TriggerCallerContext
{
[JsonProperty("awsSdkVersion")]
public string AwsSdkVersion { get; set; }
[JsonProperty("clientId")]
public string ClientId { get; set; }
}
public abstract class AbstractTriggerBase<TRequest, TResponse>
where TRequest: AbstractTriggerRequest
where TResponse: AbstractTriggerResponse
{
[JsonProperty("version")]
public int Version { get; set; }
[JsonProperty("triggerSource")]
public string TriggerSource { get; set; }
[JsonProperty("region")]
public string Region { get; set; }
[JsonProperty("userPoolId")]
public string UserPoolId { get; set; }
[JsonProperty("callerContext")]
public TriggerCallerContext CallerContext { get; set; }
[JsonProperty("request")]
public TRequest Request { get; set; }
[JsonProperty("response")]
public TResponse Response { get; set; }
[JsonProperty("userName", NullValueHandling = NullValueHandling.Ignore)]
public string UserName { get; set; }
}
public class PreSignUpSignUpRequest : AbstractTriggerRequest
{
[JsonProperty("validationData")]
public Dictionary<string,string> ValidationData { get; set; }
}
The Lambda function then ends up with the following signature:
public class Function
{
public PreSignUp_SignUp FunctionHandler(PreSignUp_SignUp input, ILambdaContext context)
{
context.Logger.LogLine("Auto-confirming everything!");
input.Response = new PreSignUpSignUpResponse {
AutoConfirmUser = true,
// you can only auto-verify email or phone if it's present in the user attributes
AutoVerifyEmail = input.Request.UserAttributes.ContainsKey("email"),
AutoVerifyPhone = input.Request.UserAttributes.ContainsKey("phone_number")
};
return input;
}
}
Hopefully this helps anyone else running into issues writing Lambda triggers for Cognito.
The previous two responses are now inaccurate unless you still use the old, less performant Amazon.Lambda.Serialization.Json.JsonSerializer. This old serializer uses Newtonsoft.Json while the new Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer implements the recent System.Text.Json.
As a result, a JObject parameter is no longer appropriate and should instead be replaced with JsonElement. If you try to use JObject with this new serializer, you will get an error since the serializer doesn't know how to deal with this object.
You should read this to gain a better understanding of how it all works, but you access properties of the JsonElement using GetProperty("[insert property name here]").
For example:
public async Task<JsonElement> FunctionHandler(JsonElement input, ILambdaContext context)
{
var request = input.GetProperty("request");
var userAttributes = request.GetProperty("userAttributes");
string email = userAttributes.GetProperty("email").GetString();
return input;
}
This way, you don't need to construct entire classes to accommodate the required request and response parameters, just get and set the properties you need.
There is already another great answer in here. However I'm not a expert .NET developer so this solution makes more sense to me.
class AutoVerifyEmail
{
public AutoVerifyEmail() { }
public JObject AutoVerifyEmailPreSignup(JObject input, ILambdaContext context)
{
//Console.Write(input); //Print Input
input["response"]["autoVerifyEmail"] = true;
input["response"]["autoConfirmUser"] = true;
return input;
}
}