Zulu-Inuoe/jzon

coercing `parse`

Zulu-Inuoe opened this issue · 2 comments

A common feature in JSON parsers is automatic object mapping into language data types such as structures, classes, etc.

While not everyone (myself included) likes automatic conversions like this, it is without question incredibly useful & convenient when developing & doing exploratory programming.

For example, consider the following:

(defpackage #:com.inuoe.jzon.coerce-example
  (:use #:cl)
  (:local-nicknames
   (#:jzon #:com.inuoe.jzon)))

(in-package #:com.inuoe.jzon.coerce-example)

(defclass coordinate ()
  ((reference
    :initarg :reference)
   (x
    :initform 0
    :initarg :x
    :accessor x)
   (y
    :initform 0
    :initarg :y
    :accessor y)))

(defclass object ()
  ((alive
    :initform nil
    :initarg :alive
    :type boolean)
   (coordinate
    :initform nil
    :initarg :coordinate
    :type (or null coordinate))
   (children
    :initform nil
    :initarg :children
    :type list)))

(let ((obj (jzon:parse (jzon:stringify (make-instance 'object :coordinate (make-instance 'coordinate))) :type 'object)))
  (with-slots (alive coordinate children) obj
    (print alive) ; nil
    (print coordinate) ; #<coordinate ...>
    (print children) ; nil

    (with-slots (x y) coordinate
      (print (slot-boundp coordinate 'reference)) ; nil
      (print x) ; 0
      (print y) ; 0
      )))

An alternative approach is to offer object coercion post-parse on the returned hash-table. And yet a third approach is to provide both options: allow a :type option to parse, but also export a coerce-value or equivalent function that users can use on say, a hash table.

Given the following JSON

{  
   "authenticationResultCode":"ValidCredentials",  
   "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png",  
   "copyright":"Copyright © 2012 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.",  
   "resourceSets":[  
      {  
         "estimatedTotal":1,  
         "resources":[  
            {  
               "__type":"ElevationData:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1",  
               "elevations":[1776,1775,1777,1776],  
               "zoomLevel":14  
            }  
         ]  
      }  
   ],  
   "statusCode":200,  
   "statusDescription":"OK",  
   "traceId":"8d57dbeb0bb94e7ca67fd25b4114f5c3"  
}

from Bing Maps API

This allows the following:

(defclass elevation-data ()
  ((elevations
    :type (vector integer))
   (|zoomLevel|
    :type integer)))

(let* ((data (json:parse "..."))
       (data (gethash "resourceSets" data))
       (data (gethash "resources" data))
       (data (aref data 0))
       (obj (jzon:coerce-into data 'elevation-data)))
  (format t "Zoom level is ~D~%" (slot-value obj '|zoomLevel|)))

where we don't need to coerce parse the full object hierarchy, just the deeply nested elevation-data object

In my experience with REST APIs, often there is "Validate and Serialize" step in JSON request processing,
and it usually is separated from JSON parsing. This usually requires creating an additional object,
usually descendant of some Serializer class.

Your approach is to construct object from json using information about class slot definition.
This is very handy but this can possibly skip validation step and cause some unwanted code execution,
for example allocation of an array of huge size in object's constructor.

Also approach with serializer as a separate object can do more smart conversions to user-defined types.
And it is more robust when you are changing object implementation.

In my opinion, getting validation and serialization right is a difficult topic, personally I would suggest
releasing v1 without any support for coercion.

I will also refresh my knowledge of validator and serializer libraries in CL and write here my thoughts about them.
Maybe jzon's readme can just recommend some other library.

Thank you for the feedback, it's really appreciated.

As somebody who uses C# regularly, I've become very attached/biased to how Newtonsoft JSON in particular goes about doing things, and ultimately with jzon I am looking to replicate the same general decisions made there.

While I understand the dangers of doing all sorts of automagic coercion, it is as you said, very handy and applicable for many purposes when you "own" the incoming JSON or can otherwise trust it. Couple that with a JSON schema and you get a good enough safety for most uses where you don't get sent malicious JSON.

That all said, you may be right for a v1 without it but my only trouble with that is: Am I offering enough different from all the other JSON libraries in CL right now? Because I don't really think so.
So I feel I need this and/or #3 - which is essentially an implementation of JObject from Newtonsoft JSON - implemented before I can feel comfortable having this out in public.