perri.to: Un mejunje de cosas

Generando tipos de go a partir de json

  2020-11-13


Uno de los obstáculos más comunes al programar software con exposición a internet en Go es el parseo de JSON. Como lenguaje tipado, Go requiere un receptor debidamente definido para el JSON deserializado. Un workaround común es un árbol anidado infinito de map[string]interface{} y un montón de type assertions recursivas.

Mientras desarrollaba atlassian-connect-go me choqué con la misma pared: muchos de los tipos de Atlassian estaban definidos únicamente como JSON. Terminé arreglándolo creando LazyApiCoder, que puede crear un conjunto completo de tipos de go a partir de un esquema swagger 3 o de un conjunto de ejemplos JSON.

Mi enfoque:

Desarrollé buena parte del plugin usando la fantástica herramienta JSON to Go de @mholt6. Tenía una mezcla de tipos a partir de ejemplos ofrecidos en distintas partes de la documentación de Atlassian. Decidí que quería tener todos los tipos posibles. Escribí este script de python que me traía todos sus ejemplos (al menos los visibles). Sobre eso encaré la primera iteración de LAC: automatizar el workaround habitual.

Mi primer intento es lo que ahora se puede invocar como LAC --target schema.go --package apackagename --source "generators/jira/jira/*.json". Este enfoque tosco deserializa todo el json en un tipo muy genérico (map[string]interface{}) y hace type assert de forma recursiva. Si el ejemplo es lo suficientemente bueno, la struct resultante es razonable. La nomenclatura es un poco cuestionable y termina desambiguando tipos distintos pero con nombres iguales con alguna lógica barata. De todos modos, va a resolver tu problema si todo lo que tenés es una muestra.

Después de haber usado ese punto de partida para construir buena parte del plugin, me choqué con la segunda pared. Al hacer un ajuste muy fino del plugin, encontré que a algunos tipos les faltaban campos. Al mirarlo un poco, descubrí que los ejemplos eran los que fallaban: simplemente no eran lo suficientemente exhaustivos, o yo no me había topado con un ejemplo de mi caso de uso.

Versión mejorada

Al leer mi script de python, me di cuenta de que había un lugar mucho mejor de donde tomar esta información. Atlassian (y muchos otros proveedores) tiene una especificación swagger 3. Una spec de swagger contiene suficiente información como para elaborar tipos de Go bastante decentes. Los nuevos tipos podían tener un Type muy preciso, Comments y estar seguros de ser tan exhaustivos como la API alguna vez necesitara.

La nueva versión se invoca como LAC --target schema.go --package apackagename --swaggerfile generators/jira/jira/swagger-v3.v3.json y toma un solo archivo por vez, dada la naturaleza de las descripciones de swagger.

Como Swagger es una spec muy documentada, todo lo que tuve que hacer fue leerla y, para los casos en que no era tan clara, consultar ejemplos conocidos. Había programado mi primera versión usando un formato intermedio, un conjunto de diccionarios de go que se usaba para producir el código final. Mi única tarea fue crear un conversor de Swagger al formato intermedio. El resultado es un par de funciones cortas que interpretan swagger y construyen una representación de tipos en memoria. El renderer tuvo que aprender algunos trucos, especialmente para embeber tipos. Los mayores desafíos fueron:

  • las cláusulas anyOf, allOf y oneOf que se traducían a varios tipos puntero embebidos en un campo/tipo
  • los tipos únicamente $ref que también terminaban siendo tipos embebidos como resultado
  • el referenciado recursivo, que se hacía usando punteros a sí mismo.
  • el additionalProperties usado en una propiedad de tipo object únicamente con $ref, que terminaba necesitando interpretarse como map[string]ReferencedType
  • las propiedades sin nombre/vacías que terminaban siendo algún sabor de interface{} o map[string]interface{} o []interface, que se deserializan bien pero le presentan más desafíos al usuario final.
  • asegurarse de que las descripciones llegaran a los comentarios para todos los tipos correctos

Podés encontrar una buena muestra de los resultados en este esquema de JIRA.

Un lindo efecto secundario de esto es que podés usarlo como una invocación de go generate: si tu código contiene una spec de swagger, obtenés tipos gratis.

Conclusión

Hay otras soluciones que intentan trabajar con swagger, algunas abandonadas, algunas en desarrollo y al menos una en uso. Mi problema con todas las librerías existentes para esto es que quieren dar soporte a todo Swagger. A mí no me interesaba integrar endpoints ni generar servidores de go, y mucho menos clientes. Mi objetivo era simple: crear tipos para trabajar con APIs descriptas en Swagger, y creo que esto es muy exitoso en su objetivo. Estoy seguro de que hay bugs y algunos errores de interpretación. Voy a ir agregando más flags con el tiempo para modificar el comportamiento y permitir interpretaciones libres de las partes no tan estrictas.

Sentite libre de mandar bugs, comentarios y PRs, van a ser bien recibidos.