I have a fairly good understanding of when data validation should and shouldn't exist when it comes when writing an application that is only dealt with by an end-user, and all the different tiers of the application and what code should check what (or, I would at least hope that I have a good idea).
However, as I've begun writing code that is not only going to interact with data provided by an end-user, but which will have developers (other than myself) specifying parameters and data to it, I've come across this conundrum numerous times. When should I validate data? When should I let it break? When is validation too little, or too comprehensive in scope? What about performance; how may it be hindered by additional layers of validation?
To provide a concrete example (out of many), one such case I had was when I was building a component that would manage a single-page web application. This component would accept a JSON object representing the web application's navigation map, in which were defined routes and the URLs at which page content resided for each route. It would use this tree of data to register the appropriate routes with a client-side Javascript router, and then handle loading and presenting pages when the right route was selected. When doing this, I encountered the following hypothetical scenario:
- Developer specifies two pages in the navigation map, let's say
page-1
andpage-2
for example. - Developer defines
page-1
to have the route#!/path
. - Developer also defines (mistakenly?)
page-2
to have#!/path
as its route.
The problem here is that, for the two pages in the example above, there's no way for my component to determine which of the two pages to load, as both have the same route: https://localhost/my-app/#!/path
leads to two entirely different pages! My component would only be aware of the last page with that registered route, as it would overwrite the definition of the first page for that route.
At first, having shortly before worked with a bunch of code related to user input validation, I added logic into the recursive function which parsed the navigation tree to maintain a list of existing routes/paths, and if a conflict was found, append an incrementing number to the end to avoid conflicts. So, for example, page-1
's route would stay #!/path
, but when parsing the next page, the code would note that it already encountered #!/path
as a route and rename the route to #!/path-2
.
I didn't think much of it at the time. But recently, as I was going back to port the code to a new format, I was looking over this part of the initialization function and I wasn't sure if it was the right thing to do.
So far, the farthest I've come to a solution to instances like these, is the thought that perhaps both methods of conquering this problem, validation and the lack thereof, have their own pros and cons.
Also, and I feel this is important to note, I'm not only talking about situations where I fix an error for a developer. I find myself in the same situation when I need to decide when to check for an error in developer-provided data that is going to another component that is encapsulated within my component. When should I let the mistake go unchecked by my code, and let the library/component I'm using throw an error on its own? When should I step in and validate the data first before relaying it over?
The pros I've thought of are:
- The code continues to work even when an error is made, which may be desirable for an environment where downtime is a big problem.
- A developer can use the error-correction behavior to their advantage, and rely on it as a sort of shorthand, in the sense of having to code less.
- Similar to point 2, the developer doesn't have to watch their code using my API for errors/omissions/mistakes as strictly, which may fit their environment and the applications more flexibly, making it less likely for an issue to come up when using another product/component, or may just make it easier for them to manage the application if there are many similar lines of code using my API.
The cons I've thought of are:
- This could lead to undesired behavior as an error could go unnoticed for a long time without throwing any errors. Depending on the circumstances, it may make finding the source of the bug tougher to find.
- If developers begin relying on the error-correction mechanism I implement, that means that as more people depend on it working the way it does, the more likely it is for a new version of my element to break functionality for a lot of people. This may make improving my library/component/API tougher over time.
- Additional validation logic at such a low level may potentially hinder performance. In some cases, this may not be noticeable if at small scale, but when used at larger scale, may become a significant problem.
- Validation logic and error-correction logic further complicates my code. This may complicate maintenance as well, and with more moving parts, the more points of failure.
I'm not sure what's a good workflow to follow to get to a resolution in this case, and what rules/guidelines I should follow when working with such systems. I've certainly learned that developing a self-encapsulated program is much easier than developing an extensible system!
I've tried looking up information to help me figure out this problem, but so far the vast majority of what I've found relates only to working with end-user input, not developing APIs or libraries for other developers to use. With end-user-provided data, I need to be worried about my application breaking. But, in this case, I don't have that worry - instead it's the developer who wrote their own program using my library that will encounter an issue - with their program. How much should I be concerned with their application breaking due to their own mistake?
So, in conclusion:
- What steps should I follow to determine, when developing APIs or extensible components, when I should or when I should not correct mistakes validate data provided by other developers?
- How should I determine when my code should let 3rd-party data go unchecked to a deeper API/program component not written by me to let it throw an error, and when it should step in and check for errors first before relaying the data?
Footnote:
To be clear, I understand there isn't always (or, rather, is rarely) a one-size-fits-all solution, and I am not asking for the "best option." Really what I'm looking to understand is how do I evaluate which route to take in this case, and what variables may I be not considering? From the experience of others who have worked with similar situations, what did you learn along the way, and what should I do to figure this problem out? I'm coming at this without any practical experience working in an office or on a project team, only experience with the technology itself. The more I've developed, the more I've learned that there is far more to programming than just the code itself.