Generating different structs for different error codes
We use server code generation with the strict-server: true parameter. During generation, we obtain the following structures for the response
GetHealthcheck200JSONResponse{
Timestamp: time.Now().UnixMilli(),
}
GetHealthcheck500JSONResponse{
Timestamp: time.Now().UnixMilli(),
}
And this happens with absolutely any response, a separate structure is created for each error code specified in the API contract. We find this inconvenient, but couldn't figure out how to make a generic structure or our own wrapper. Could you suggest how to be in this situation?
Having the same gripe with the generation. I have the following example
/v1/stuff/{stuffName}:
get:
description: Get details about a specific stuff
security:
- Bearer: []
parameters:
- name: stuffName
in: path
required: true
schema:
type: string
minLength: 1
description: Name of the stuff to retrieve details for
responses:
"200":
description: Successfully retrieved stuff details
content:
application/json:
schema:
$ref: "#/components/schemas/StuffDetails"
"400":
$ref: "#/components/responses/BadRequestError"
"401":
$ref: "#/components/responses/UnauthorizedError"
"404":
$ref: "#/components/responses/NotFoundError"
"500":
$ref: "#/components/responses/InternalServerError"
I get the following response object:
type GetV1StuffStuffNameResponseObject interface {
VisitGetV1StuffStuffNameNameResponse(w http.ResponseWriter) error
}
But the InternalServerErrorJSONResponse structure generated for the #/components/responses/InternalServerError component, doesn't satisfy interface GetV1StuffStuffNameResponseObject. If the struct InternalServerErrorJSONResponse would get the necessary methods for satisfying all the interfaces for each endpoint response, we could return in our method directly our generic responses (example: InternalServerErrorJSONResponse)
Does it make sense? If they would extend the generation to add methods to satisfy all the custom response object interfaces, it would greatly simplify error handling.
I resolved this by creating a custom template for strict-interface.gotmpl, that generates:
type LoginUser401JSONResponse UnauthorizedErrorJSONResponse
instead of
type LoginUser401JSONResponse struct{ UnauthorizedErrorJSONResponse }
After this PR https://github.com/oapi-codegen/oapi-codegen/pull/2110 gets merged, we will have support for converting strings to numbers, which helps conditionally handle the generation of response structs based on status codes in the temple.
Something like this works for me:
{{if gt ($statusCode | atoi) 399 -}}
type {{$receiverTypeName}} {{$ref}}{{.NameTagOrContentType}}Response
{{else -}}
type {{$receiverTypeName}} struct{ {{$ref}}{{.NameTagOrContentType}}Response }
{{end}}
Or u can completely remove the error structs (ur choice), but since I had a common utility function to generate error response interfaces, the above solution works for me.
EDIT:
The above dint work for me. I am still using a custom template, and this time I've completely removed all error structs and interface impl for those structs and instead returning nil, err in my handlers.
I have a custom error handler that handles these errors and convert it to the correct error response json.
The way I found around the duplicated error everywhere was to errors.as against an expanded error interface
type RespError interface {
error
WriteResponse(w http.ResponseWriter) error
Status() int
}
If this is satisfied, the type would generate a custom writer for it.
I also added a helper to convert any data I wanted into an error that implemented RespError, WrapError(err error, code int, data any) error which would set both the code and object that would get output (the object being encoded into json, tho renaming to WrapErrorJSON and adding a variant WrapErrorXML would be easy to do).
Because it was an interface, that also made it easier to use across multiple generated libraries too. Didn't have to deal with complexity putting my "common error types" in one package, and using them in another anymore.
@phoenisx mind sharing your custom template ?
You can find it here: https://gist.github.com/phoenisx/6a2e9aefc2e5f0fa3822688b37db4eef
Though I must say I have a different approach than how oapi handles errors here. Instead of returning an Error object as a response, I throw a custom error type, which I use to parse before returning a 4xx/5xx error.
This way I can keep using error interface and also have the benefit of using strict handlers.
I think the overall architecture could still be improved, but I've left it as is for now, as I was wasting a lot of time architecting, then actually working on the APIs.