Introduction à Barley, le sérialiseur éclair

Les origines
L’entreprise pour laquelle je travaillais avait une API vieillissante en v1 qui était très mal conçue et lente dans son implémentation. Elle servait 4 plateformes frontend (un site web, deux applications mobiles et un outil d’administration). Étant dans un environnement de startup, notre équipe devait aller vite, mais c’était au détriment de la performance et de la maintenabilité. C’était la situation quand je suis arrivé.
Retourner des objets JSON était très fastidieux. On utilisait une méthode custom disponible dans chaque modèle qui construisait un Hash imbriqué, basé sur les attributs que vous lui donniez. C’est vite devenu un vrai bordel à gérer avec de mauvaises performances et des implications de sécurité préoccupantes. C’était difficile à maintenir, le nommage des variables et le comportement de la fameuse méthode étaient étranges, et donc nous avons entrepris de nous en débarrasser et sommes passés à une sorte de DSL de sérialiseur qui a été progressivement introduit pour remplacer l’usine à gaz.
Plus tard, nous avons décidé de refactorer notre API avec une nouvelle version pour nous débarrasser de cet encombrante dette technique. Quelques exigences ont rapidement émergé, parmi lesquelles :
- L’API V2 devrait être facile à maintenir, avec des conventions de nommage claires et un namespacing
- Puisqu’il s’agit d’une API interne, elle devrait être étroitement couplée à l’utilisation et à la plateforme prévues
- L’API V2 devrait utiliser une sorte de DSL de vue pour générer les payloads JSON
- L’API V2 devrait être aussi prévisible que possible en termes de typage et de formatage
- L’API V2 devrait être super rapide par défaut. Ce devrait être la dernière chose que nous devons optimiser
J’ai donc examiné le DSL de sérialiseur que nous avions conçu pour l’API v1. La première chose que j’ai faite a été de le benchmarker contre d’autres solutions. J’ai donc retiré les fonctionnalités héritées / qui n’étaient plus nécessaires, l’ai déplacé vers une gem locale, et ai commencé à jouer avec quelques benchmarks.
Et c’était très rapide en effet ! Voici le résultat d’un benchmark que j’ai fait à l’époque :
Warming up --------------------------------------
# Omis pour la concision
Calculating -------------------------------------
ams 67.770 (± 1.9%) i/s - 657.000 in 10.042270s
jsonapi-rb 157.239 (± 2.3%) i/s - 1.521k in 10.010257s
barley 96.909 (± 2.4%) i/s - 945.000 in 10.036316s
barley-cache 4.589k (± 2.8%) i/s - 44.620k in 10.010004s
ams eager 68.237 (± 6.2%) i/s - 592.000 in 10.087602s
jsonapi-rb eager 289.733 (± 3.7%) i/s - 2.805k in 10.033912s
barley eager 406.732 (± 3.2%) i/s - 3.922k in 10.020076s
barley-cache eager 639.935 (± 2.4%) i/s - 6.300k in 10.053710s
with 95.0% confidence
Comparison:
barley-cache : 4589.3 i/s
barley-cache eager: 639.9 i/s - 7.17x (± 0.27) slower
barley eager: 406.7 i/s - 11.29x (± 0.49) slower
jsonapi-rb eager: 289.7 i/s - 15.83x (± 0.74) slower
jsonapi-rb : 157.2 i/s - 29.17x (± 1.08) slower
barley : 96.9 i/s - 47.37x (± 1.75) slower
ams eager: 68.2 i/s - 67.25x (± 4.69) slower
ams : 67.8 i/s - 67.72x (± 2.31) slower
with 95.0% confidence
Calculating -------------------------------------
# Omis pour la concision
Comparison:
barley-cache : 46090 allocated
barley-cache eager: 216790 allocated - 4.70x more
barley eager: 354326 allocated - 7.69x more
jsonapi-rb eager: 694430 allocated - 15.07x more
jsonapi-rb : 926082 allocated - 20.09x more
ams eager: 1068038 allocated - 23.17x more
barley : 1102354 allocated - 23.92x more
ams : 1299674 allocated - 28.20x more
Ce qui est plutôt sympa 😎.
Je suis partageur
J’ai donc fait une gem de notre sérialiseur et l’ai publiée sur Github. Elle s’appelle Barley.
J’ai récemment rendu la gem encore plus rapide (à partir de la v0.9.0) et l’ai benchmarkée contre Alba, Blueprinter, Turbostreamer, Jserializer, FastSerializer, Representable, SimpleAMS, AMS, Rails JBuilder et Panko.
Voici les résultats de ces nouveaux benchmarks :
ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +YJIT +PRISM [x86_64-linux]
Warming up --------------------------------------
# Omis pour la concision
Calculating -------------------------------------
alba 401.119 (± 1.7%) i/s (2.49 ms/i) - 2.016k in 5.027350s
alba_with_transformation
256.123 (± 1.2%) i/s (3.90 ms/i) - 1.300k in 5.076497s
alba_inline 22.749 (± 8.8%) i/s (43.96 ms/i) - 114.000 in 5.049973s
ams 16.923 (± 5.9%) i/s (59.09 ms/i) - 85.000 in 5.029061s
barley 427.045 (± 2.6%) i/s (2.34 ms/i) - 2.162k in 5.066130s
barley_cache 378.285 (± 2.4%) i/s (2.64 ms/i) - 1.927k in 5.097462s
blueprinter 120.704 (± 1.7%) i/s (8.28 ms/i) - 612.000 in 5.071371s
fast_serializer 142.347 (± 1.4%) i/s (7.03 ms/i) - 720.000 in 5.058924s
jserializer 252.455 (± 1.6%) i/s (3.96 ms/i) - 1.274k in 5.047584s
panko 510.262 (± 4.7%) i/s (1.96 ms/i) - 2.585k in 5.077381s
rails 107.589 (± 7.4%) i/s (9.29 ms/i) - 546.000 in 5.104715s
representable 50.264 (± 6.0%) i/s (19.89 ms/i) - 252.000 in 5.027987s
simple_ams 36.584 (± 5.5%) i/s (27.33 ms/i) - 184.000 in 5.054347s
turbostreamer 351.551 (± 1.7%) i/s (2.84 ms/i) - 1.776k in 5.053401s
Comparison:
panko: 510.3 i/s
barley: 427.0 i/s - 1.19x slower
alba: 401.1 i/s - 1.27x slower
barley_cache: 378.3 i/s - 1.35x slower
turbostreamer: 351.6 i/s - 1.45x slower
alba_with_transformation: 256.1 i/s - 1.99x slower
jserializer: 252.5 i/s - 2.02x slower
fast_serializer: 142.3 i/s - 3.58x slower
blueprinter: 120.7 i/s - 4.23x slower
rails: 107.6 i/s - 4.74x slower
representable: 50.3 i/s - 10.15x slower
simple_ams: 36.6 i/s - 13.95x slower
alba_inline: 22.7 i/s - 22.43x slower
ams: 16.9 i/s - 30.15x slower
Calculating -------------------------------------
# Omis pour la concision
Comparison:
panko: 259178 allocated
barley: 633521 allocated - 2.44x more
turbostreamer: 641760 allocated - 2.48x more
alba_with_transformation: 818181 allocated - 3.16x more
alba: 818241 allocated - 3.16x more
jserializer: 822281 allocated - 3.17x more
barley_cache: 849521 allocated - 3.28x more
fast_serializer: 1470121 allocated - 5.67x more
blueprinter: 2297921 allocated - 8.87x more
alba_inline: 2736041 allocated - 10.56x more
rails: 2757857 allocated - 10.64x more
ams: 4713401 allocated - 18.19x more
representable: 5151321 allocated - 19.88x more
simple_ams: 9017033 allocated - 34.79x more
Avec YJIT activé, Barley brille vraiment et est le premier sérialiseur Hash du groupe. Il y a plus de benchmarks à trouver dans le repository Barley.
Barley est très simple dans son implémentation et alloue très peu de mémoire. Je crois que c’est ce qui le rend si rapide. J’écrirai un autre article sur comment je l’ai optimisé dans la version 0.9.0.
Les fonctionnalités
Barley fournit :
- un DSL clair pour définir votre sérialiseur
- définition de sous-sérialiseur inline / block
- russian-doll caching
- type-check avec dry-types
- gestion du contexte
Voici comment Barley fonctionne :
Dans le modèle que vous voulez sérialiser
D’abord, incluez le concern Barley::Serializable dans votre modèle.
# /app/models/user.rb
class User < ApplicationRecord
include Barley::Serializable
end
Une fois que vous incluez ce module, Barley cherchera une classe UserSerializable dans votre codebase, vous devez donc en définir une, typiquement dans le répertoire app/serializable. Elle devrait hériter de Barley::Serializer
# /app/serializers/user_serializer.rb
class UserSerializer < Barley::Serializer
end
Une fois que cela est configuré, la méthode as_json sur votre modèle retournera ce que vous avez défini dans votre classe de sérialiseur.
Vous pouvez également choisir d’utiliser un sérialiseur différent pour rendre vos données. Vous pouvez utiliser la méthode serializer dans votre modèle. Cette méthode vous permet également de passer un argument cache.
# /app/models/user.rb
class User < ApplicationRecord
include Barley::Serializable
serializer MyCustomSerializer, cache: true
# ou
serializer MyCustomSerializer, cache: {expires_in: 1.hour}
end
Une autre façon d’y parvenir est via la méthode as_json :
User.first.as_json(serializer: MyCustomSerializer, cache: {expires_in: 1.hour})
Notez bien que Barley remplace la méthode as_json et ne lui fournit pas les arguments standards only, include ou except. C’est parce que le but de ce sérialiseur est précisément de faire ce travail. L’argument root fonctionne, cependant.
Définir le sérialiseur
Revenons à la classe de sérialiseur. Barley fournit un DSL simple qui vous permet de définir votre JSON de manière lisible et maintenable. Voici un exemple de définition qui couvre la plupart des cas d’utilisation :
# /app/serializers/user_serializer.rb
class UserSerializer < Barley::Serializer
attributes id: Types::Strict::Integer, :name
attribute :email
attribute :value, type: Types::Coercible::Integer
many :posts
one :group, serializer: CustomGroupSerializer
many :related_users, key: :friends, cache: true
one :profile, cache: { expires_in: 1.day } do
attributes :avatar, :social_url
attribute :badges do
object.badges.map(&:display_name)
end
end
end
Pour que la vérification de type fonctionne, vous devez définir un module Types comme ceci, quelque part dans votre codebase :
module Types
include Dry.Types()
end
1. Attributs
Ils peuvent être définis comme un tableau avec la méthode plurielle attributes. Ce peut être un tableau de symboles, ou un tableau de hashes dans le pattern key: TypeDefinition, ou un mélange des deux.
Ils peuvent également être définis avec une méthode singulière attribute, qui accepte un argument de mot-clé type.
Si type est inclus dans la définition d’un attribut, il lèvera une erreur. Vous pouvez utiliser des types pour contraindre les types et définir des contraintes. Veuillez vous référer à la gem dry-types pour plus d’options.
Si vous donnez un bloc à la méthode attribute, vous pouvez également retourner tout ce que vous voulez. Vous avez l’objet object à portée de main qui fait référence au modèle sérialisé.
2. Associations
Elles sont définies avec les méthodes one ou many.
Cela utilisera le sérialiseur par défaut si aucun argument n’est donné. Vous pouvez lui fournir un serializer et une option cache.
Ces méthodes ne fournissent pas d’argument type, car cela n’a aucun sens de le faire.
Vous pouvez également décider de lui donner un bloc, qui vous permet de définir le sérialiseur de l’association inline.
Pour conclure
L’objectif de la gem Barley est de fournir un DSL simple pour définir vos payloads JSON avec une option de type-proof, et de les rendre de manière très rapide et efficace. Elle n’est probablement pas aussi riche en fonctionnalités que d’autres sérialiseurs disponibles pour la communauté Rails, mais c’est une option sans prise de tête, mais puissante que vous devriez considérer si vous recherchez la performance et la simplicité.
Je serais heureux de voir plus de projets commencer à l’adopter et j’invite la communauté à proposer des optimisations pour la rendre encore meilleure.
MoskitoHero