Background
I have a set of configuration JSON files that look like the following:
{
"version" : 1.0,
"startDate": 1548419535,
"endDate": 1558419535,
"sourceData" : [...] // nested json inside the List.
"destData" : [...] // nested json inside the List.
"extra" : ["business_type"]
}
There are several such config files. They are fixed and reside in my code directory only. The internal representation of each config file is given by my case class Config
:
case class Attribute(name: String, mappedTo: String)
case class Data(location: String, mappings:List[Attribute])
case class Config(version: Double, startDate: Long, endDate: Long, sourceData: List[Data],
destData: List[Data], extra: List[String])
I have three classes Provider
, Parser
and Validator
.
Provider
has a methodgetConfig(date: Long): Config
. It has to return the config satisfyingstartDate <= date <= endDate
(ideally exactly one such config should be present, asstartDate
toendDate
defines the version of config to be returned).getConfig
calls a method insideParser
calledparseList(jsonConfigs: List[String]): Try[List[Config]]
. WhatparseList
does is try to deserialize all configs in the list, each to an instance of case classConfig
. Even if one JSON fails to deserializeparseList
returns ascala.util.Failure
otherwise it returnsscala.util.Success[List[Config]]
.- If
scala.util.Success[List[Config]]
is returned from the previous step,getConfig
then finally calls a method insideValidator
calleddef validate(List[Config], Date): ValidationResult[Config]
, and returns it's result. As I want all errors to be accumulated I am using Cats Validated for validation. I have even asked a question about it's correct usage here. validate
does the following: Checks if exactly oneConfig
in the List, is applicable for the given date (startDate <= date <= endDate
) and then performs some validations on thatConfig
(otherwise it returns aninvalidNel
). I perform some basic validations on theConfig
like checking various Lists and Strings being non empty etc. I also perform some semantic validations like checking that each String in fieldextra
is present inmappings
of eachsource/dest Data
etc.
Question
- The question that has troubled me for couple of last days is, my purpose for using
Cats Validated
was solely to collect all errors (and not to fail fast when encountering the first validation error). But by the time I reachvalidate
method I have already done some kind of validations inparseList
method. That is, I have already validated inparseList
that my JSON structure is in accordance to my case classConfig
. But myparseList
doesn't accumulate errors like myvalidate
method. So if many incompatibilities between my json structure and my case classConfig
are present I'll get to know only the first. But I would like to know them all at once. It gets worse if I start adding
require
clauses likenonEmpty
inside the case class only ( they will be invoked while construction of case class, i.e. while parsing itself), e.g.case class Data(location: String, mappings: List[Attribute]) { require(location.nonEmpty) require(mappings.nonEmpty) }
So I am not able to draw a line between my parsing and my validation functionality properly.
- One solution I thought of was abandon the current JSON library (lift-json) I am using and use play-json instead. It has functionality for accumulating errors like
Cats Validated
(I got to know about it here, goes really well with CatsinvalidNel
). I thought I would first parse JSON to play-json's JSON ASTJsValue
, perform the structural compatible validation betweenJsValue
and myConfig
using play-jsonsvalidate
method (it accumulates errors). If its fine readConfig
case class fromJsValue
and perform latter validations I gave examples of above, using Cats. - But I need to parse all config to see which one is applicable for a given date. I don't proceed if even one config fails to deserialize. If all deserialize successfully I pick the one whose
(startDate, endDate)
enclose the given date. So if I follow the solution I mentioned above, I have pushed the conversion ofList[JsValue]
toList[Config]
to validation phase. Now if eachJsValue
in the List deserializes successfully to aConfig
instance, I can choose the applicable one, perform more validations on it and return the result. But if someJsValue
fail to deserialize what do I do? Should I return their errors? Doesn't seem intuitive. This problem here is that I need to parse all config to see which one is applicable for a given date. And this is making it more difficult for me to mark a separation between parsing and validation phase.
How do I draw a line between parsing and validating a config in my scenario? Do I change the way I maintain versions (a version being valid from start to end date)?
PS: I am an extremely novice programmer in general. Forgive me if my question is weird. I myself never thought I would spend so much time on validation while learning Scala.