Le schéma

Les différentes versions de la proposition de norme, ainsi que des exemples sont disponibles dans les pages web associées à la liste de discussion. On trouvera de nombreuses informations complémentaires sur le site de Kris Zyp, qui est à l'origine de la proposition.

Un schéma JSON définit les propriétés de l'objet sérialisé et les contraintes sur leurs valeurs. La description d'une structure JSON est codée en JSON, elle est donc elle-même validable au moyen de ce schéma.

Pour chaque propriété, le schéma permet de spécifier:

  • son type : simple (chaîne, nombre, booléen etc) ou composé. Par exemple, si une propriété peut contenir un entier ou un booléen, on indiquera {"type":["boolean","integer"]}. Le type peut être précisé en faisant référence à un format parmi ceux pré-définis ici. La liste des formats pré-définis est longue: date, heure, uri, adresse mail, etc;
  • si la propriété est un objet, la liste de ses propriétés;
  • si la propriété est un tableau, le schéma permettant de valider chacun de ses items;
  • on peut indiquer des relations entre les propriétés, par exemple que l'une est obligatoire si une autre est renseignée. Dans l'exemple {"state":{optional:true},"town":{"requires":"state",optional:true}}, la ville et l'état sont optionnels, mais si la ville est présente l'état devra l'être également;
  • différentes caractéristique: présence obligatoire, schéma pour valider d'éventuelles propriétés additionnelles, etc. On peut également signifier que la valeur d'une propriété devra être unique pour tout l'objet. C'est typiquement le cas d'une propriété id;
  • les valeurs autorisées pour une propriété sont définissables de plusieurs manières: en précisant un intervalle, les longueurs minimales et maximales pour les chaînes de caractères, une énumération de valeurs autorisées, un masque utilisant des expression régulière... Pour un tableau, on pourra préciser son nombre minimum et maximum d'items;
  • le schéma contient enfin quelques propriétés ignorées par la validation, mais qui servent à définir plus précisément l'objet et pourront être utilisées dans d'autres contextes: la visibilité de la propriété (publique ou privée), est-ce qu'il est permis de la modifier, est-ce que sa valeur est volatile ou devrait persister...

Un mécanisme d'héritage permet de dériver de plusieurs autres schémas. De plus, JSON Schema utilise la syntaxe JSON Referencing, une autre proposition de Kris Zyp dont j'ai parlé dans mon précédent billet. On peut donc dans un schéma faire référence à un autre schéma.

La proposition n'est pas encore finalisée, mais a déjà suscité des implémentations dans de nombreux langages, en JavaScript évidemment, en Python et depuis peu en PHP, grâce au travail de Bruno Reis.

Un petit exemple de schéma

Cet exemple, extrait de la documentation définit un évènement de calendrier:

  {"description":"A representation of an event",
    "type":"object",
    "properties":{
      "dtstart":{
        "format":      "date-time",
        "type":        "string",
        "description": "Event starting time"
      },
      "summary":{
        "type": "string"
      },
      "location":{
        "type":     "string",
        "optional": true
      },
      "url":{
        "type":"string",
        "format":   "url",
        "optional": true
      },
      "dtend":{
        "format":      "date-time",
        "type":        "string",
        "description": "Event ending time",
        "optional":    true
      },
      "duration":{
        "format":      "date",
        "type":        "string",
        "description": "Event duration",
        "optional":true
      },
      "rdate":{
        "format":      "date-time",
        "type":        "string",
        "description": "Recurrence date",
        "optional":    true
      },
      "rrule":{
        "type":        "string",
        "description": "Recurrence rule",
        "optional":    true
      },
      "category":{
        "type":     "string",
        "optional": true
      },
      "description":{
        "type":     "string",
        "optional": true
      },
      "geo":{
        "type":       "object",
        "properties": {"$ref":"http://json-schema.org/geo.properties"},
        "optional":   true
      }
    }
  }

Exemples d'applications

JSON Schema à tous les étages

JSON Schema permet en fait bien plus que valider format de données. Certaines de ses propriétés n'ont pas grand chose à voir avec de la validation. C'est un schéma pour décrire de manière relativement exhaustive n'importe quel objet manipulable dans un traitement informatique. Dès lors, on peut l'utiliser comme format de description des objets métier d'une application, un format aisé à utiliser à tous les niveaux. Dans le cadre d'une architecture MVC, on pourra par exemple l'utiliser dans les vues pour générer dynamiquement les formulaires de gestion de l'objet. Passé au client, il pourra être utilisé pour réaliser une première validation en JavaScript des valeurs saisies par l'utilisateur. Côté serveur à nouveau, dans le contrôleur, on re-validera les saisies au moyen du même shéma, ce qui garantit la cohérence des tests entre le client et le serveur. En cas d'import de données, c'est encore lui qui contrôlera la validité des informations importées. Et bien sûr, dans la couche Modèle, le schéma servira de base au mapping avec la base de données.

La plupart des frameworks proposent certes déjà des mécanismes similaires, en décrivant les objets métier en XML, voire en YAML comme dans Symfony. Mais l'alternative JSON Schema ma parait séduisante.

Exemple funky

Le billet d'Aaron Boodman m'a donné une autre idée d'utilisation de JSON Schema, pour contrôler les paramètres d'une méthode.

Les paramètres nommés sont une fonctionnalité bien pratique de certains langages, Python par exemple. Ils permettent lors de l'appel d'une méthode de spécifier ses paramètres en les nommant, et non à partir de leur position. Pratique lorsque la signature d'une méthode commence à s'allonger. Pour pallier l'absence de cette fonctionnalité, j'ai l'habitude de remplacer les longues listes de paramètres par un unique objet en JavaScript, ou un tableau en PHP. La signature de la méthode perd en clarté (et l'assistance à la saisie des IDE est également à la peine), mais je trouve le code d'appel plus lisible:

// à
$foo->bar('toto', 'titi', 'tata', 'tutu');
// je préfère
foo.bar({firstname; 'toto', lastname: 'titi'});
$foo.bar(array(firstname; 'toto', lastname: 'titi'));
foo.bar(lastname= 'titi', firstname = 'toto');

Autre inconvénient de PHP et JavaScript, ils sont faiblement typés. Préciser le type des paramètres est impossible en JS et seulement possible pour les objets en PHP. Avec JSON Schema, et en passant les paramètres sous forme de tableau ou d'objets, on va pouvoir pour le même prix les valider.

Attention, le code qui suit va sans doute faire hurler quelques-uns tant il utilise un missile de croisière pour écraser une mouche. Mais comme vous le diront nombre de militaires, c'est bien plus fun qu'avec une tapette. Dans cet exemple, avant d'appeler une fonction, je valide chacun de ses paramètres au moyen d'un schéma JSON. Accessoirement, je retrouve le type des paramètres grâce aux annotations. Merci de ne pas chercher la petite bête, le code a été écrit sur le coin d'une table juste pour illustrer mon propos[1].

  <?php
  require_once 'JsonSchema.php';
  require_once 'JsonSchemaUndefined.php';

  Class toto{


    /**
     * my test function
     *
     * @param param JSON my_schema
     *
     * @return void
     */
    public function titi($param){
      echo "bonjour monde cruel\n";
    }
  }

  /**
   * Create an object from an array
   */
  function toObject($arr){
    $res = new StdClass();
    if (is_array($arr)) {
      foreach ($arr as $k => $v) {
        $res->$k = $v;
      }
      return $res;
    } else {
      return $arr;
    }
  }
  // calendar event description, taken from http://groups.google.com/group/json-schema/web/common-json-schema-definitions
  $my_schema = '
  {"description":"A representation of an event",
   "type":"object",
   "properties":{
    "dtstart":{"format":"date-time","type":"string","description":"Event starting time"},
    "summary":{"type":"string"},
    "location":{"type":"string","optional":true},
    "url":{"type":"string","format":"url","optional":true},
    "dtend":{"format":"date-time","type":"string","description":"Event ending time","optional":true},
    "duration":{"format":"date","type":"string","description":"Event duration","optional":true},
    "rdate":{"format":"date-time","type":"string","description":"Recurrence date","optional":true},
    "rrule":{"type":"string","description":"Recurrence rule","optional":true},
    "category":{"type":"string","optional":true},
    "description":{"type":"string","optional":true},
    "geo":{"type":"object","properties":{"$ref":"http://json-schema.org/geo.properties"},"optional":true}
    }
  }
  ';
  $method  = new ReflectionMethod('toto', 'titi');
  $comment = $method->getDocComment();
  // use a regular expression to extract the parameters type from the method comment
  preg_match_all("/^\s*\*\s*@param\s*(\[^\s\]+)\s*(\[^\s\]+)\s*(\[^\s\]+)/m", $comment, $params, PREG_SET_ORDER);
  foreach ($params as $param){
    if (count($param) $gt; 2){
      if ($param[2] == 'JSON') {
        if (isset($$param[3])) {
          $schema = json_decode($$param[3]);
        }
      }
    }
  }
  // some unit tests
  if (!empty($schema)){
    $tests = array(
              // invalid
              'toto',
              // invalid
              array(),
              // invalid
              array('dtstart' => '', 'summary' => '', 'geo' => ''),
              // still invalid but should validate as the validator 
              // is in beta and not very accurate
              array('dtstart' => '1970-01-01T00:00:00',
                    'summary' => 'Epoch',
                    'geo'     => toObject(array()))
              );
    foreach ($tests as $n => $test) {
      echo "\n\nTEST n°$n\n\n";
      var_dump($test);
      $res = JsonSchema::validate(toObject($test), $schema);
      if ($res->valid){
        toto::titi($param);
      } else {
        foreach ($res->errors as $error) {
          echo sprintf("Wrong parameter : %s %s\n", $error['property'], $error['message']);
        }
      }
    }
  }

Preuve que le validateur PHP n'est pas encore tout à fait au point, le dernier exemple "passe" alors qu'il est invalide. Mais bon, l'esprit est là.

J'espère que ce billet vous a donné des idées, n'hésitez pas à les partager.

Notes

[1] désolé pour la lisibilité du code, Gandi qui héberge ce blog ne fournit pas de plugin de coloration syntaxique