Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strictly decode to case class disallowing extraneous fields in JSON #104

Open
LeifW opened this issue May 20, 2014 · 17 comments
Open

Strictly decode to case class disallowing extraneous fields in JSON #104

LeifW opened this issue May 20, 2014 · 17 comments

Comments

@LeifW
Copy link

LeifW commented May 20, 2014

So that

case class Person(name: String)
Parse.decode[Person]("""{"name": "Bob", "foo": "dunno"}""")

will yield something about "Error: unexpected field "foo"".

Is this feasible?

Example use case: When people call our API, it would be helpful to tell them if the misspelled a key or added junk that doesn't do anything - tell them that's not key that means anything to us and it'll be ignored / rejected as invalid.

@markhibberd
Copy link
Contributor

@markhibberd
Copy link
Contributor

I will leave this open, as a reminder to add an example in the docs. Let me know if this doesn't address what you need.

From your example:

  implicit def PersonDecodeJson: DecodeJson[Person] = 
   casecodec1(Person.apply, Person.unapply).validate(....)

Also let me know if there is a general formulation that would let you not specify the validation function explicitly. It is worth noting that it used to be the default, but it was changed because some found it not desirable - but also because it generated fairly poor error messages - if we could address the poor errors part, I would be happy to add some more combinators to make it easy to validate without specifying a custom error message.

@markhibberd markhibberd mentioned this issue Jun 28, 2014
17 tasks
@lukasz-golebiewski
Copy link

@markhibberd The URL you pasted is dead. Do you have any examples on how to use validate?

@kevinmeredith
Copy link
Contributor

kevinmeredith commented Apr 2, 2017

To make sure that I understand this problem, @LeifW, let's please consider an example.

Given:

import argonaut._, Argonaut._

case class Person(name: String)

implicit def decode: DecodeJson[Person] =
  DecodeJson ( c => 
    for {
      name <- (c --\ "name").as[String]
    } yield Person(name)
  )

scala> Parse.decode[Person]("""{"name": "Bob", "foo": "dunno"}""")
res5: Either[Either[String,(String, argonaut.CursorHistory)],Person] = Right(Person(Bob))

you would expect, either by default in Parse#decode or an optional Parse#decodeExactly, that the extra foo field should result in a Left rather than Right?

@LeifW
Copy link
Author

LeifW commented Apr 2, 2017 via email

@seanparsons
Copy link
Member

DecodeJson.validate should be able to achieve that (albeit at a cost to the caller). It does chime slightly with an idea I've had in the past for a different way to handle parsing completely but behaviour like this would sit weirdly in our current model for decoding.

@kevinmeredith
Copy link
Contributor

kevinmeredith commented Apr 6, 2017

Thanks, @seanparsons, for pointing me to that method!

Locally, I added the following:

case Some(w) => println(a); DecodeResult.ok(w)

to this line.

Then, I re-ran the above example via sbt console:

JSON contains exact # of fields, one, as case class to which it's decoded

scala> Parse.decode[Person]("""{"name": "Bob"}""")
HCursor(CObject(CJson({"name":"Bob"}),false,object[(name,"Bob")],(name,"Bob")),CursorHistory(List(El(CursorOpDownField(name),true))))
res0: Either[Either[String,(String, argonaut.CursorHistory)],Person] = Right(Person(Bob))

JSON has one extra field, i.e. not in the Person

scala> Parse.decode[Person]("""{"name": "Bob", "foo": "dunno"}""")
HCursor(CObject(CJson({"name":"Bob","foo":"dunno"}),false,object[(name,"Bob"),(foo,"dunno")],(name,"Bob")),CursorHistory(List(El(CursorOpDownField(name),true))))
res1: Either[Either[String,(String, argonaut.CursorHistory)],Person] = Right(Person(Bob))

After decoding, i.e. JSON => Person, is it possible to figure out if the *Cursor has any extra, i.e. not walked, fields?

I looked at the scaladocs for HCursor and ACursor, but I'm not sure. Please help me out?

@seanparsons
Copy link
Member

@kevinmeredith Not walked? I think you'd be better off just checking that fieldSet matches what you expect.

@kevinmeredith
Copy link
Contributor

matches what you expect

Could you please say more?

Taking your reply, @seanparsons, I posted this StackOverflow question - http://stackoverflow.com/questions/43433054/retrieving-traversed-json-fields.

I appreciate any help, please.

@seanparsons
Copy link
Member

seanparsons commented Apr 17, 2017

@kevinmeredith This is what you want to check against correct?

scala> json.right.get.hcursor.fieldSet
res1: Option[Set[argonaut.Json.JsonField]] = Some(Set(foo))

@kevinmeredith
Copy link
Contributor

kevinmeredith commented Apr 18, 2017

@seanparsons Given:

case class Person(name: String)

and:

scala> Parse.parse("""{"name": "Bob", "foo": "dunno"}""").toEither.right.get.hcursor.fieldSet
res6: Option[Set[argonaut.Json.JsonField]] = Some(Set(name, foo))

How would you use fieldSet to determine if there's any extraneous/extra fields in the input JSON? As far as I understand, there's no getFields on a case class, but please correct me if there is.

Also, my original thought was add an def decodeExactly function that did:

  1. Get list of fields not walked/traversed - http://stackoverflow.com/questions/43433054/retrieving-traversed-json-fields
  2. Decode JSON to case class
  3. if step 2 succeeds, check if 1 is empty. If it's non-empty, then return an error that there's extra fields

Thanks for your continued help.

@seanparsons
Copy link
Member

Well surely the fieldSet you want is Some(Set("name")) right?

Unless I'm mistaken that means in the DecodeJson.validate call the function HCursor => Boolean would look something like _.fieldSet == Some(Set("name")).

To support a decodeExactly we'd need to do a non-trivial amount of changes because of the way the existing code is written.

@kevinmeredith
Copy link
Contributor

kevinmeredith commented Jul 8, 2017

Given all of the fields of a case class A and all of the Json's keys, assuming we decode the Json to an A, we can compare A's fields to Json's keys to implement this decodeExactly behavior, no?

https://stackoverflow.com/a/27445097/409976 shows how, using shapeless, we can get all of a case classes' fields w/ LabelledGeneric.

With respect to the second problem, i.e. getting all keys from a Json, I'm not sure if such a method exists. I don't believe that Cursor#fields would work, as is:

scala> Json.obj(
     "a" -> jString(""), 
     "b" -> jBool(false), 
     "c" -> Json.obj("a" -> jNull) 
).cursor.fields
res27: Option[List[argonaut.Json.JsonField]] = Some(List(a, b, c)) // where's `c`'s a?

Does a allFields method, which would find all fields from the Json recursively, exist? If not, perhaps a PR could implement that method, and then, using LabelledGeneric and the aforementioned method, we could address this issue?

Finally, if you all find this approach to be technically sound, then perhaps it belongs in https://github.com/alexarchambault/argonaut-shapeless?

@seanparsons
Copy link
Member

@kevinmeredith The fields and fieldsSet methods gives you the fields of the object currently in focus. So if you want to check that it has only fields a, b and c you can validate against that.

Hence this:

scala> val testDecoder: DecodeJson[(String, Boolean)] = jdecode2L((a: String, b: Boolean) => (a, b))("a", "b")
testDecoder: argonaut.DecodeJson[(String, Boolean)] = argonaut.DecodeJson$$anon$4@3f59fa4e

scala> val improvedTestDecoder = testDecoder.validate(_.fieldSet == Some(Set("a", "b")), "Wrong fields")
improvedTestDecoder: argonaut.DecodeJson[(String, Boolean)] = argonaut.DecodeJson$$anon$4@45ed447c

scala> improvedTestDecoder.decodeJson(Json("a" := "q", "b" := true))
res7: argonaut.DecodeResult[(String, Boolean)] = DecodeResult(Right((q,true)))

scala> improvedTestDecoder.decodeJson(Json("a" := "q", "b" := true, "c" := 1))
res8: argonaut.DecodeResult[(String, Boolean)] = DecodeResult(Left((Wrong fields,CursorHistory(List()))))

@kevinmeredith
Copy link
Contributor

As I understand your reply, @seanparsons, you believe the LabelledGeneric approach that I proposed in #104 (comment) to be unnecessary given the solution that you've provided with DecodeJson#validate?

If so, perhaps I should ask if the application of LabelledGeneric would be appropriate to https://github.com/alexarchambault/argonaut-shapeless, i.e. to derive such a DecodeJson without the need to call DecodeJson#validate manually?

@seanparsons
Copy link
Member

@kevinmeredith I was pointing out the manual solution, a generic solution would likely want to invoke the same kind of code. It would definitely be a nice thing to have in argonaut-shapeless but that's not really a question for me! :)

@alexarchambault
Copy link
Contributor

alexarchambault commented Jul 11, 2017

That's definitely do-able with argonaut-shapeless. It has some JsonProductCodec, that can be customized to check for any non-decoded field. One should just roughly adapt back these lines to argonaut-shapeless, and ensure the implicit they define is in scope when the DecodeJson are derived. Then decoding would fail if any extra field is present.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants