Question

We're implementing a REST service with three main layers: Controller, Domain, Repository.

The REST API is supposed to respond to error conditions with meaningful error messages. Suppose the general layout of a payload given to a POST request were something like this:

{  "name": { "first": "string", "last": "string" } }

Now, say we have an error somewhere down the line, and name.first is rejected for whatever reason. We're following the JSON API standard and would like to give source pointers to the payload object:

{
  "errors": [
    {
      "status": "400",
      "source": { "pointer": "/name/first" },
      "title":  "Invalid Value",
      "detail": "'Steve' is not a valid name."
    }
  ]
}

But the entities in the repository and domain may look very different from the controller entities.

So, how do I report adequately which fields in the original JSON are affected when an error is raised in the repository?

I'm using Kotlin. Say my repository data structure looks like this:

data class Person(val firstName: String, lastName: String)

whereas in the controller it probably looked something like this:

data class PersonInput(name: Name)
data class Name(first: String, last: String)

So if, say, my data base backend rejects firstName I now need to raise an error about Person.firstName in the repository that the controller needs to map to its personInput.name.first value, and report the correct { "source": { "pointer": { "/name/first/" } }. It can't just report firstName. The user has never seen it, and that's an implementation detail of my repository.

Right now, all options I have are kind of bad:

Raise very specific errors, that are always mapped to a particular REST input field

That's an OK approach for few and simple data structures, but it breaks down once the data structures become dynamic, or even overly complex, since you'll end up with a zoo of enums/error types.

Annotate all values that pass down to the controller with source information

Since I can't have annotations with retention RUNTIME and target EXPRESSION, i.e. I can't "enhance" values by stealthily adding a field via reflection, I'd need to wrap every value in my entire logic everywhere with a special type, say

data class Source<T>(val content: T, val source: String)

This adds a ton of indirection everywhere and makes my domain and repository hellish to test, as I need always wrap everything in pointless source pointers.

Have a look up table that co-ordinates domain and repository entities with REST source pointers

The idea would be to map repository entity fields and domain entity fields to controller entity fields in an explicit lookup table, whether through strings or even reflection (property names.)

Now there's an object in my program that knows about all layers, and breaks every time I change anything in any of the layers. That's not good!


Bottom line: We're stumped. How do we create useful error messages for our clients without breaking architecture encapsulation between layers? We haven't found good answers on the Net.

Ideally, I'd find a way to seamlessly annotate values with information about where they came from, without disrupting the models of the inner layers of the architecture.

Was it helpful?

Solution

You are probably trying to generate the JSON error information at the wrong level in your architecture.

If you convert data from one format to another at a layer boundary (like going from PersonInput to Person), then you should also catch all errors that come back over that layer boundary and perform a reverse mapping as needed. That is the only way that you can properly localize the knowledge that Person.firstName and PersonInput.name.first contain the same information.

This means that all error objects should contain the relevant information in a format that is easily processed by your program and only when constructing the reply message to be sent over HTTP should that information be transformed into a JSON API error object (unless JSON objects can be handled directly by your code).

OTHER TIPS

You had an error, and you received an error message that described in some detail what went wrong. Nice for you. But what are you actually doing with it? When you have an error message, you may use it to fix the problem at runtime (are you doing that?). You can somehow show it to the developer ( through logs, or directly if the developer runs the code) to help them improve the code. You can show a message to the user either saying “sorry, you can’t have this”, or “something went wrong, here is how to fix it”.

And that’s the angle where you need to start. Start with what action you want to take: Fixing it, logging it, displaying something useful to the user. Find out what you need to achieve this. And then you transform the error in the way needed.

For logging (that is helping the developer to improve the code), it’s probably best to have the message as original as possible.

Licensed under: CC-BY-SA with attribution
scroll top