2

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.

  • Why don't you reuse the same data structs through out the application? – Ewan Apr 4 at 10:43
  • Well, that would totally go against the Clean Architecture pattern (as I understand it) and lead to other problems, such as tight coupling between the REST input model and the repository implementation: every time something changes in the REST, domain, or repository, the models may need to reflect it, and break the other two layers in the process. – Aleksandar Dimitrov Apr 4 at 10:45
  • strictly speaking no, its loose coupling. But already have that coupling because you assume that your objects can be serialised/deserialized into each other. Using the same data structure will solve your problem and save you a whole lot of lines of code. – Ewan Apr 4 at 10:49
  • Yup, the client should receive a list of failing rules. It's fail-fast in the sense that it fails as early as possible, but some validations happen in parallel (hence the list of errors.) Our API is supposed to be as easy as possible to use, which is why the PO's requirement is that we indeed have a fine-grained error handling. – Aleksandar Dimitrov Apr 5 at 6:58
  • You have already devalued the possible solution by investing so many efforts for so little benefit. Just send a message the end-user can understand. Right now, you are at the edge of the overengineering and the ROI for this seems uncertain. – Laiv Apr 5 at 7:50
6

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).

  • Thanks for your suggestion. The problem with that is that I'll need a process to interpret error results at boundary layers, to map them back to the local entity model. I fear this could get relatively complex and fragile to maintain. So far, I think it's our best option, but it doesn't sound like a particularly easy one. – Aleksandar Dimitrov Apr 4 at 11:51
  • 1
    Do type the errors. Let the use case interpreter throw exceptions. Catch the exceptions later as possible @inputs' layer. Map errors to pointers. Exceptions can bear as many info as other layers might need to resolve the mapping (property keys, ids, codes, etc) – Laiv Apr 4 at 12:50
2

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.

  • Sorry if I wasn't clear enough. Logging is handled separately. This is about showing the person who uses our REST API clear and helpful error messages. The crux is pointing the user to the source where the error messages originate from, i.e. indicating the exact field that contains the offending content. – Aleksandar Dimitrov Apr 5 at 6:55
  • you are overthinking this. On one side, tech-savvy users don't know what to do with the pointer. On the other side, devs would know what to do with a simple "invalid lastName". – Laiv Apr 5 at 7:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.