Our C# SDK for the Marqeta Core API, generated by Kiota.
Tip
If you're here because the sky is falling and you need to make a very quick fix to the SDK without modifying the script please see instructions here.
For complete reference API documentation, see the Marqeta Core API Reference.
For reference on Kiota, from tooling, to using the generated client and its concepts please visit the MS docs here, or the GitHub repository.
The tool will install a local tool for Kiota as defined in .config/dotnet-tools.json.
The SDK generation script also builds and tests the generated client. To run tests locally, user secrets need to be configured to use your developer public sandbox instance.
In the Marqeta.Core.Sdk.Tests directory, run the following commands:
dotnet user-secrets init
dotnet user-secrets set "Marqeta:BaseUrl" "https://sandbox-api.marqeta.com/v3/"
dotnet user-secrets set "Marqeta:UserName" "<Application token>"
dotnet user-secrets set "Marqeta:Password" "<Access Token>"
You can obtain a developer public sandbox to obtain the needed credentials by signing up to marqeta as per instructions here.
Execute the dotnet fsi GenerateSdkFromSourceUrl.fsx command in the root source directory. This will execute the F# script via F# interactive.
- Downloads the latest CoreAPI.yaml from Marqeta OpenAPI repository.
- Parses it with OpenAPI.NET.
- Saves the parsed file to disk as
Marqeta.Core.SdkSourceCoreAPI.yaml, this allows us to keep formatting and ordering consistent for easier diffs. - Applies a variety of modifications to the OpenAPI specification.
- Validates the modified specification.
- Saves the modified specification to disk as
Marqeta.Core.Sdk/CoreAPI.yaml. - Installs tools specified in
.config/dotnet-tools.json(currently only Kiota). - Invokes Kiota to generate a C# client in
Marqeta.Core.Sdk/Generated, if a client already exists (denoted by the presence ofkiota-lock.json), it'll update the existing client. - Builds the solution and run all tests.
Nuget packages needed for the script to run
- Check for new versions of the packages referenced at the top of file prefaced with
#r - Check for breaking changes
- Update versions if appropriate
- Re-run script
- Commit and push
The Kiota dotnet CLI tool used to generate the output SDK from the OpenAPI specification file
- Check for relevant breaking changes on the GitHub Release Page
- Run the command
dotnet tool update microsoft.openapi.kiotain the root directory of repository to update the tool - Run the
GenerateSdkFromSourceUrl.fsxscript - If build and tests succeed then commit and push
Kiota nuget packages added as a dependency to the Marqeta SDK project
- Check for relevant breaking changers on the GitHub Release Page
- Update all Kiota nuget packages
Microsoft.Kiota.* - Diff the following files with the same file of each found in Kiota dotnet GitHub
Marqeta.Core.Sdk/Serialization/Json/CustomJsonParseNode.csMarqeta.Core.Sdk/Serialization/Json/CustomJsonParseNodeFactory.csMarqeta.Core.Sdk/Serialization/Json/TypeConstants.csMarqeta.Core.Sdk/Serialization/Text/CustomTextParseNode.csMarqeta.Core.Sdk/Serialization/Text/CustomTextParseNodeFactory.cs
- Pay attention to changes listed in serialization changes, as well as any comments starting with
// Modified: - Bring our versions of these files inline where appropriate to make sure we're following Kiotas general standards for serialization and for bug/performance fixes
- If build and tests succeed then commit and push
Miscellaneous nuget packages added as a depdendency to the Marqeta SDK project for things like IoC (also covers nuget packages for the Marqeta.Core.Sdk.Tests project)
- Check for relevant breaking changes
- If versions have updated, then update these where appropriate
- If build and tests succeed then commit and push
Note
When updating Kiota it is best to update the CLI and packages together in the same release as the Kiota generation depends on its abstractions and the generated SDK may require changes not present in our versions of the Kiota nuget packages.
- In the
GenerateSdkFromSourceUrl.fsxscript, make changes in theOpenApiHelpersmodule, there are a lot of examples of modifications in there already, so if you're unsure follow an existing example.- There are a lot of functions for different sections, mostly following the hierarchical structure of the OpenAPI specification, with functions for specific schema models (e.g.
#/components/schemas/transaction_model,#/components/schemas/card_holder_model). - Ensure the changes have relevant comments.
- There are a lot of functions for different sections, mostly following the hierarchical structure of the OpenAPI specification, with functions for specific schema models (e.g.
- Execute the script to generate the new SDK and validate your changes.
- These should be visible in the
Marqeta.Core.Sdk/CoreAPI.yamldiff, and also in the generated code SDK.
- These should be visible in the
- Add tests if needed, and validate these pass.
- Update documentation (this README for example).
- Commit and push.
Important
Make sure to include the changes to kiota-lock.json, SourceCoreAPI.yaml, and CoreAPI.yaml in your commit.
Caution
If for some reason the script isn't working, or we need to make a quick and dirty fix due to a production issue, we can make a very quick change with the following instructions.
Please note that this should only be done as a last resort, and any changes MUST be added to both the script and documentation in a subsequent PR as soon as possible.
- Make your change directly to the
Marqeta.Core.Sdk/CoreAPI.yamlfile. - Ensure the required dotnet tools are installed locally by executing the
dotnet tool restorecommand. - Execute the
kiota updatecommanddotnet tool run kiota update -o Marqeta.Core.Sdk/Generated --clean-output --clear-cache. - Build the solution and run all tests.
- Commit and push.
Note
- Kiota performs type trimming to remove unused types, so it won't generate models that aren't directly referenced, if models are missing, please manually add a representative model to
Marqeta.Core.Sdk/Extensions - Kiota doesn't handle different response content types for the same status code, Marqeta can return HTML error responses sometimes, this is unstructured data so we can't bind it to a model, out of the box this information is lost, however, we have added a
text/htmlparser to manually add this data to ourApiErrormodel. This can be found inMarqeta.Core.Sdk/Serialization/Text.- This change makes it so that on calling
GetObjectValue<T>for theIParseNode, will create a newApiErrortype (if applicable) and set theMessageEscapedto the HTML text returned.
- This change makes it so that on calling
- Kiota doesn't generate enum path parameters (for C#, it was added to other languages), we don't have a workaround yet, so during usage we're converting the enum to it's string representation, one problem is that this causes the enum types not to be generated, the webhook
EventTypefor example is not generated, so we've manually added this enum, an issue is raised on the Kiota GitHub repository here. - The default Kiota JSON deserialization implementation will populate
nullif it can't parse a value, this obviously isn't great for us, we want to have loud shouting errors if we're unable to correctly parse a response rather thannullvalues, so we've implemented our ownIParseNodefor JSON inMarqeta.Core.Sdk/Serialization/Json(modified version of the default Kiota JSON deserialization implementation), and there is an issue raised on the Kiota GitHub repository here. - The generated client doesn't have an interface, which makes unit testing difficult, please refer to Kiota unit testing docs for more information.
- Global (applied to all models)
- Mark properties as
readonlyfalse, some requests have properties set asreadonlytruewhich breaks SDK generation, meaning we can't set these values. - Done in the
applySchemaPropertiesModificationsfunction.
- Mark properties as
#/components/schemas/mcc_group_modeldocs- Change the property
mccfrom an array of objects to an array of strings. - Done in the
applyMccGroupModelModificationsfunction.
- Change the property
#/components/schemas/card_holder_modeldocs- Add a new missing property
status, this is an enum that adds the following values:UNVERIFIED,LIMITED,ACTIVE,SUSPENDED,CLOSED - Done in the
applyCardHolderModelModificationsfunction.
- Add a new missing property
#/components/schemas/transaction_modeldocs- Add missing enum values to the
typeproperty, the missing values added are:address.verification,authorization.clearing.representment,billpayment,billpayment.clearing,billpayment.reversal,fee.charge.pending.refund,transaction.unknown - Done in the
applyTransactionModelTypeModificationsfunction.
- Add missing enum values to the
#/components/schemas/transaction_model/transaction_metadataJIT Funding decision: Transaction Metadata docs, Transaction docs- Add the
EU_MOTO_NON_SECUREenum topayment_channelproperty, this is because Marqeta keep sending it via webhooks, although this is meant to be an internal enum, and causes transactions to fail deserialization. - Done in the
applyTransactionMetadataPaymentChannelModificationsfunction.
- Add the
- Remove the
#/components/schemas/BadRequestError,#/components/schemas/Error,#/components/schemas/ForbiddenError,#/components/schemas/InternalServerError,#/components/schemas/UnauthorizedErrormodels from the schema. This is because most paths/operations in the OpenAPI specification don't have any error models defined, there's also the fact we don't want an error model per response code, Kiota adds the response status code to the baseApiExceptionfor us, so we created our own sharedApiError(mentioned below).- Done in the
removeUnusedErrorSchemasfunction.
- Done in the
- Add a new
#/components/schemas/ApiErrormodel, this has the propertieserror_codeanderror_messageon it, which bind to the API error response typically returned by Marqeta (note their docs don't explicitly mention this format).- Done in the
addErrorSchemafunction.
- Done in the
#/components/schemas/posdocs- Add the
UNSCHEDULED_CARD_ON_FILEmissing enum value to thetransaction_initiated_categoryproperty. This value is sent via webhooks, and causes deserialization errors if absent (note: this value is not documented as of writing). - Done in the
applyPosModelModificationsfunction.
- Add the
#/components/schemas/Terminal_model- Add the
UNSCHEDULED_CARD_ON_FILEmissing enum value to thetransaction_initiated_categoryproperty. This value is sent via webhooks, and causes deserialization errors if absent (note: this value is not documented as of writing). - Done in the
applyTerminalModelModificationsfunction.
- Add the
- Adds/replace default response on all operations for all paths to be
ApiError.- This specifies that all unspecified responses are to try to bind to
ApiError, in practice this means all4XXand5XXresponses, but could include other unhandled response codes. - Done in the
addOrReplaceDefaultErrorResponsefunction.
- This specifies that all unspecified responses are to try to bind to
- Remove all existing
4XXand5XXresponses on all operations for all paths.- This is because we add a default response of
ApiErrorourselves, most of the4XXand5XXresponse specifications are actually empty objects anyway, so won't generate anything to bind to.
The only operations that currently have a valid response specification schema are thePOST /feedback/fraudendpoint, but we remove these and use our own model (they're removed from#/components/schemastoo as part of schema model modifications mentioned above). - Done in the
applyOperationsModificationsfunction.
- This is because we add a default response of
- Remove all examples for every response and request for all paths and operations.
- These don't actually add any value to the SDK generation, but they do create a lot of noise in validation output due to the examples not matching the specification in a lot of cases.
- Done in the
applyRequestModifications→removeOpenApiMediaTypeExamples,applyResponseModifications→removeOpenApiMediaTypeExamplesfunctions.
- Add the authorization reversal path to path list manually
- This endpoint is currently undocumented. A method has been added to append it the paths present if it is not added by Marqeta
- Done in the
addAuthorizationReversalPathfunction
As alluded to in the Gotchas and known issues section, we've had to add some custom deserializers to support our needs.
Kiota doesn't use standard deserialization methods, but have instead opted to use a common interface across all languages supported by its generator, there are some docs on this.
However, the tl;dr is that we need an IParseNodeFactory as well as an IParseNode for each MIME type we want to deserialize (application/json and text/html) in our case.
For these to get used by the generated client they need to be specified in the kiota-lock.json as below:
"deserializers": [
"Marqeta.Core.Sdk.Serialization.Text.TextHtmlParseNodeFactory", // Our text/html parse node factory
"Marqeta.Core.Sdk.Serialization.Json.CustomJsonParseNodeFactory", // Our application/json parse node factory
"Microsoft.Kiota.Serialization.Text.TextParseNodeFactory",
"Microsoft.Kiota.Serialization.Form.FormParseNodeFactory"
]These are added as part of the original kiota generate command by adding the following arguments to the command --deserializer Marqeta.Core.Sdk.Serialization.Text.TextHtmlParseNodeFactory and --deserializer Marqeta.Core.Sdk.Serialization.Json.CustomJsonParseNodeFactory Deserializer argument docs, Serializer argument docs.
Important
Specifying deserializers (or serializers for that matter) as part of the kiota generate command will remove all defaults, so you need to add the other required options manually like so --deserializer Microsoft.Kiota.Serialization.Text.TextParseNodeFactory.
If updating an existing SDK, anything already in the kiota-lock.json will be used, so if you need to add a new serializer/deserializer for an update of a client, manually add it there.
The implementation for text/html is borrowed from the default Kiota TextParseNodeFactory and TextParseNode supplied in Microsoft.Kiota.Serialization.Text GitHub, and can be found in Marqeta.Core.Sdk/Serialization/Text.
We change the GetObjectValue<T> to check if the type we're trying to deserialize into is of ApiError, if so we just put the contents of the _text property on the IParseNode into ApiError.MessageEscaped.
The implementation for application/json is borrowed from default Kiota JsonParseNodeFactory and JsonParseNode supplied in Microsoft.Kiota.Serialization.Json GitHub, and can be found in Marqeta.Core.Sdk/Serialization/Json.
The main changes we've made here is to remove the safety around parsing so it will fail loudly, this is because by default Kiota will return null for values it can't parse, this doesn't quite work for us.
So instead we remove all the safety checks and wrap the field assignments in AssignFieldValues<T> in a try-catch to throw a JsonException when we fail to parse.
We also customised the JsonSerializerOptions with JsonSerializerDefaults.Web in our CustomJsonParseNodeFactory which gets set on the KiotaJsonSerializationContext.
Lastly we've added a check for an empty/null enum before parsing in GetEnumValue<T>() so that we don't throw an exception if no enum value is provided.