in spring api rest snake case json ~ temps de lecture.

Formatter correctement les données de vos API REST

Lors de la mise en place du design d'une API il est un point essentiel à prendre en compte, même si ce n'est pas celui auquel on pense en premier, il s'agit du formattage en sortie de votre flux de données.
Votre API doit être auto-descriptive dans tous les sens du terme, dans son utilisation et dans la lecture des données retournées.
C'est sur ce deuxième point que porte la suite de ce billet.

JSON / XML

  • Le format XML est verbeux mais présente un avantage indéniable à mes yeux, à savoir le typage fort des données et la possibilité d'insérer des contraintes personnalisées (XML + XSD). Mais ça s'arrête là.

  • Je ne peux que conseiller d'utiliser plutôt le format JSON, qui est plus léger, plus lisible, moins contraignant et surtout basé sur une notation objet qui le rend facile à exploiter par n'importe quel langage. JSON est finalement devenu la norme.

Regarder cette courbe étonnante qui montre un désintérêt complet pour le développement d'API basé sur l'XML contrairement à JSON : API JSON par rapport à XML :
XML API versus JSON API

Si vous souhaitez permettre à vos clients d'exploiter soit de l'XML soit du JSON, l'usage est d'ajouter une extension dans vos urls (.json ou .xml), ça a le mérite de demander clairement le format de la réponse attendue, exemple :

https://api.twitter.com/1.1/geo/reverse_geocode.json  

Voir Spring MVC content negociation pour sa mise en place dans un projet Spring.

Pretty print

Par défaut le flux de données JSON en sortie de votre API est quasiment illisible :

{"status":"UP","diskSpace":{"status":"UP","total":105555013632,"free":9659949056,"threshold":10485760}}

Il est possible d'installer un plugin au niveau de votre navigateur pour rendre lisible la donnée (ex: JSON View), mais je ne peux que conseiller de faire en sorte que cela soit natif à votre API, ainsi peu importe la nature du client qui vous interroge, la sortie sera toujours lisible :

{
    status: "UP",
    diskSpace: {
        status: "UP",
        total: 105555013632,
        free: 9660084224,
        threshold: 10485760
    }
}

Dans une configuration Spring MVC qui utilise par défaut Jackson, il faut redéfinir un @Bean ObjectMapper de la façon suivante :

@Configuration
public class JacksonConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        return mapper;
    }
}

A noter que le poids du JSON sorti en mode pretty print est légèrement supérieur au poids du JSON sans, mais cela peut être considéré comme négligeable si la compression gzip est activée au niveau de votre serveur web, ce qui devrait être toujours le cas.

NON EMPTY / ALWAYS

Deux cas sont à considérer sur la granularité et la quantité d'informations à retourner dans la sortie JSON de votre API :

  • Tous les champs sont retournés même s'ils sont vides ou null afin de transmettre de façon exacte l'ensemble des champs possibles :
{
  "myList" : [ "111", "222" ],
  "myString" : "",
  "myObject" : null
}
  • Seuls les champs non-null/non-vides sont retournés afin de ne pas polluer le flux d'informations inutiles mais aussi surtout afin de limiter le poids du JSON :
{
  "myList" : [ "111", "222" ]
}

Je suis partisan de la deuxième méthode qui permet d'aller à l'essentiel et d'avoir un flux de données lisible par tous. Si le flux JSON reçu est transformé en objet Java par exemple, alors les champs non retournés par l'API auront une valeur par défaut qui sera probablement "null".

Dans une configuration Spring MVC qui utilise par défaut Jackson, il faut redéfinir un @Bean ObjectMapper de la façon suivante :

@Configuration
public class JacksonConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        return mapper;
    }
}

snake_case / CamelCase

  • CamelCase : Cette méthode a été démocratisée par le langage Java. Il s’agit de démarquer le début de chaque mot en mettant la première lettre en majuscule. Par ex. CamelCase, CurrentUser, AddAttributeToGroup, etc.
{
  "myList" : [ "ici c'est", "paris" ],
  "myString" : "data",
  "myObject" : 1
}
  • snake_case : Cette méthode est celle utilisée depuis longtemps en C et plus récemment en Ruby. Les mots sont séparés par des underscore « _ » permettant au lecteur humain de séparer les mots de manière quasi naturelle. Par ex. snake_case, current_user, add_attribute_to_group, etc.
{
  "my_list" : [ "ici c'est", "paris" ],
  "my_string" : "data",
  "my_object" : 1
}

Si nous souhaitons respecter à la lettre la norme JSON (JavaScript Object Notation) alors CamelCase devrait être la représentation à utiliser, cependant pour une question de lisibilité pour l'oeil humain (même si pour les différents frameworks cela ne change rien évidemment) je conseille d'utiliser snake_case et ainsi rejoindre certains géants du Web qui ont pris le même chemin :

snake_case sur le web

Dans une configuration Spring MVC qui utilise par défaut Jackson, il faut redéfinir un @Bean ObjectMapper de la façon suivante :

@Configuration
public class JacksonConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
        return mapper;
    }
}
comments powered by Disqus