This document outlines some potential gotchas when using and consuming the Schemas defined in this library as well as some samples below to help with understanding/advanced use cases.
The below links have information about the defining Schemas and how the validation is performed.
JTD Schemas for requests and responses validation (Via AJV).
In this document it may use language such as:
- MUST → this is something you always do
- MUST NOT → this is something that you should never do
- SHOULD → this is something that is a recommendation and generally is a good idea to follow to avoid issues
You may notice whilst the Schemas are being rolled out that the same object may be used for the request and the response.
However this may not be best practice and in the future it may be better to split out the request and the response requests out to have different types.
On the request you would have an optional (i.e. field?: string | null;
) property on the request.
Then on the response you could include the the field on the response as a required property (i.e. field: string | null
).
This approach would allow you to introduce a new field with backwards compatibility immediately whilst you update consumers to using the new type whilst also emitting the new property in responses.
When installing the Schemas using the LOKE cli tool if you have request/response objects that are not reference types (i.e. defined using properties
, optionalProperties
, etc) then the LOKE cli will create types for you in the following format MethodNameRequest
and MethodNameResponse
.
When defining your types (in the definitions
section) you MUST NOT define a type that matches the above format.
For example if you had the following:
// implementation
interface CreateCustomerRequest {
name: string;
}
interface Customer {
id: string;
name: string;
}
CreateCustomer({orgId: string; request: CreateCustomerRequest}): Promise<Customer>
// schema definition
{
...,
definitions: {
Customer: {
properties: {
id: { type: "string" }
name: { type: "string" }
}
},
CreateCustomerRequest: {
properties: {
name: { type: "string" }
}
}
},
methods: {
CreateCustomer: {
requestTypeDef: {
properties: {
orgId: {type: "string" }
request: { ref: "CreateCustomerRequest" }
}
},
responseTypeDef: { ref: "Customer" }
}
}
}
When installing the above with the LOKE cli it will generate the following types:
// types created by definitions
export type CreateCustomerRequest = {
name: string;
}
export type Customer {
id: string;
name: string;
}
// request type created by LOKE cli, response type is Customer above
export type CreateCustomerRequest = {
orgId: string;
request: CreateCustomerRequest
}
As you can see in the types above there are two CreateCustomerRequest
types and this is invalid (caused by
explicitly defining CreateCustomerRequest
as a type AND the LOKE cli automaticaly creating a request type for the createCustomer()
method). To avoid this you MUST ensure that your type names never match the format generated by the LOKE cli.
If a better name for type cannot be determined you SHOULD prefix or suffix the type name to avoid the clash.
Some examples that have been used are Rpc
prefix or Body
/Payload
/Details
suffix but be consistent with existing code.
To fix the above implementation/code we simply change the name of the type that we have defined ourselves (i.e. renamed CreateCustomerRequest
to CreateCustomerRequestPayload
).
NOTE: When installing the types with the LOKE cli this would still create the CreateCustomerRequest
but as we have
renamed our custom type to not match this pattern there won't be a clash.
// implementation (rename CreateCustomerRequest to CreateCustomerRequestPayload)
interface CreateCustomerRequestPayload {
name: string;
}
interface Customer {
id: string;
name: string;
}
CreateCustomer({orgId: string; request: CreateCustomerRequest}): Promise<Customer>
// schema definition
{
...,
definitions: {
Customer: {
properties: {
id: { type: "string" }
name: { type: "string" }
}
},
CreateCustomerRequestPayload: { // change type name to avoid clash
properties: {
name: { type: "string" }
}
}
},
methods: {
CreateCustomer: {
requestTypeDef: {
properties: {
orgId: {type: "string" }
request: { ref: "CreateCustomerRequestPayload" }
}
},
responseTypeDef: { ref: "Customer" }
}
}
}
When using Schemas you MUST_NOT use Date
types in your request/response objects. Instead use strings formatted to
iso8601/rfc3339 (i.e. "2023-01-20T14:22:23Z") in your implementation and types and use { type: "timestamp" }
in the
Schema.
When using the new Schemas (and serviceWithSchema
) it has been observed that returning a void
return no longer
is acceptable as this then causes the request to fail in the http-rpc-client
library due to the request not being
able to be converted to json.
If you are adding Schemas to existing rpc services you MUST update any methods that return void
to instead return
something or simply return null
(including the typescript type for the method).
In the Schema method definition you simply do not define response and this will be converted to an any
by the LOKE cli.
If a property from a rpc service is field: string | undefined
as apposed to field?: string
this may have issues
when defining the Schema. In the Schema definition to specify something as undefined is specifying that it is OPTIONAL.
When the intention is that the field is "blank", returning null
instead of undefined
SHOULD be used
(i.e. field: string | null
).
When methods have no request (this is very rare) then you simply do not define a request in the Schema definition.
This then gets set to an any
by the LOKE cli.
NOTE: When using the types generated by the LOKE cli in this cases you may have to pass an empty object as the request.
Taking the typescript type see the sample included below.
interface Location {
name: string;
maxPatrons: number;
latitude: number;
longitude: number;
website: string | null;
type: "FAST_FOOD" | "CAFE";
tags: string[];
hasDanceFloor: boolean;
createdAt: string; // iso8601/rfc3339 timestamp string
address: {
streetAddress: string;
locality: string;
region: string;
postalCode: string;
country: string;
} | null;
}
NOTE: The below definition for numbers (both integer and floating point) are using the largest sizes available and are not using signed vs unsigned values as are available in the JTD specification. Please use this example as a guide and use the appropriate JTD property types as required.
You would define this type as follows in the schema
{
properties: {
name: { type: "string" },
maxPatrons: { type: "int32" },
latitude: { type: "float32" },
longitude: {type: "float32" },
website: { type: "string", nullable: true },
type: { enum: ["FAST_FOOD", "CAFE" ] },
tags: { elements: { type: "string" } },
hasDanceFloor: { type: "boolean" },
createdAt: { type: "timestamp" },
address: {
properties: {
streetAddress: { type: "string" },
locality: { type: "string" },
region: { type: "string" },
postalCode: { type: "string" },
country: { type: "string" }
},
nullable: true
}
}
}