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.
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 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.
- In the
GenerateSdkFromSourceUrl.fsx
script, make changes in theOpenApiHelpers
module, 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.yaml
diff, 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.yaml
file. - Ensure the required dotnet tools are installed locally by executing the
dotnet tool restore
command. - Execute the
kiota update
commanddotnet 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/html
parser to manually add this data to ourApiError
model. This can be found inMarqeta.Core.Sdk/Serialization/Text
.- This change makes it so that on calling
GetObjectValue<T>
for theIParseNode
, will create a newApiError
type (if applicable) and set theMessageEscaped
to 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
EventType
for 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
null
if 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 thannull
values, so we've implemented our ownIParseNode
for 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
readonly
false
, some requests have properties set asreadonly
true
which breaks SDK generation, meaning we can't set these values. - Done in the
applySchemaPropertiesModifications
function.
- Mark properties as
#/components/schemas/mcc_group_model
docs- Change the property
mcc
from an array of objects to an array of strings. - Done in the
applyMccGroupModelModifications
function.
- Change the property
#/components/schemas/card_holder_model
docs- Add a new missing property
status
, this is an enum that adds the following values:UNVERIFIED
,LIMITED
,ACTIVE
,SUSPENDED
,CLOSED
- Done in the
applyCardHolderModelModifications
function.
- Add a new missing property
#/components/schemas/transaction_model
docs- Add missing enum values to the
type
property, the missing values added are:address.verification
,authorization.clearing.representment
,billpayment
,billpayment.clearing
,billpayment.reversal
,fee.charge.pending.refund
,transaction.unknown
- Done in the
applyTransactionModelTypeModifications
function.
- Add missing enum values to the
#/components/schemas/transaction_model/transaction_metadata
JIT Funding decision: Transaction Metadata docs, Transaction docs- Add the
EU_MOTO_NON_SECURE
enum topayment_channel
property, 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
applyTransactionMetadataPaymentChannelModifications
function.
- Add the
- Remove the
#/components/schemas/BadRequestError
,#/components/schemas/Error
,#/components/schemas/ForbiddenError
,#/components/schemas/InternalServerError
,#/components/schemas/UnauthorizedError
models 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 baseApiException
for us, so we created our own sharedApiError
(mentioned below).- Done in the
removeUnusedErrorSchemas
function.
- Done in the
- Add a new
#/components/schemas/ApiError
model, this has the propertieserror_code
anderror_message
on it, which bind to the API error response typically returned by Marqeta (note their docs don't explicitly mention this format).- Done in the
addErrorSchema
function.
- Done in 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 all4XX
and5XX
responses, but could include other unhandled response codes. - Done in the
addOrReplaceDefaultErrorResponse
function.
- This specifies that all unspecified responses are to try to bind to
- Remove all existing
4XX
and5XX
responses on all operations for all paths.- This is because we add a default response of
ApiError
ourselves, most of the4XX
and5XX
response 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/fraud
endpoint, but we remove these and use our own model (they're removed from#/components/schemas
too as part of schema model modifications mentioned above). - Done in the
applyOperationsModifications
function.
- 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
→removeOpenApiMediaTypeExamples
functions.
- 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
addAuthorizationReversalPath
function
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
.