Categorygithub.com/Lemonn/JSON2Go
repository
0.0.0-20250205112356-89768ccfc5f9
Repository: https://github.com/lemonn/json2go.git
Documentation: pkg.go.dev

# Packages

No description provided by the author

# README

JSON2Go

Overview

Json2Go could generate Golang structs from any given valid JSON document. But this is something that could be done by many tools out there. What sets Json2Go apart is the fact that it has been developed with undocumented or poorly documented JSON-APIs in mind.

The fact that such APIs change without notice and often omit empty fields. Makes it hard to generate a good matching Golang data type. Here comes Json2Go into play. It provides features tailored to overcome the hurdles of such APIs.

  • The core feature to overcome this obstacle is the ability to not only look at one but at all elements of an array. To determine the resulting datatype. But also the ability to combine a set of JSON responses into one data structure.

  • This is paired with the ability to determine the resulting type of each field, based on all seen values. Either via built-in functions or custom ones.

  • To counter the fact that sometimes a field is added or rarely seen. A soft error, which does not interrupt the unmarshalling process, is returned upon entering an unknown field.

Tag Documentation

Uses the json2go tag, to store relevant data. This includes all previously seen values for a field. Its original type, should the type have been adjusted, and references to the to and from type functions. The data is organized in a structure.

type Tag struct {
	SeenValues              []string        `json:"seenValues"`
	CheckedNonMatchingTypes []string        `json:"checkedNonMatchingTypes"`
	ParseFunctions          *ParseFunctions `json:"parseFunctions,omitempty"`
	BaseType                *string         `json:"baseType,omitempty"`
}

To store the struct as a tag, it's JSON marshalled and base64 encoded.

The data is used by Json2Go in different steps. It's also used to combine multiple generated structures into one. Therefore, the data needs to be kept, as long as the type determination is not final. After the generation has been finalized, the data could be removed. Sometimes it's required to combine a lot of data, to determine the struct type. Or even do it in production on live data. To prevent the tag from exploding. It also stores a set of strings, that contain all non-matching types. The Typeadjuster could be set, to ignore the given adjusters.

Type Adjustment

Important, it's required to unnest your generated structs first, before using the type adjustment!

Json2Go can determine the best type of set of input values using Typeadjusters. It comes with some built-in ones Build-in TypeCheckers and could also be extended with custom ones.

To use the type adjustment, first, create a slice of Typeadjusters. The order determines the priority, should more than one create a match.

TypeCheckers := []JSON2Go.TypeDeterminationFunction{&JSON2Go.TimeTypeChecker{}}

Then call the adjustment function

err := JSON2Go.AdjustTypes(file, TypeCheckers)
if err != nil {
	fmt.Println(err)
	return
}

To use a custom adjuster, implement the following interface

type TypeDeterminationFunction interface {
	CouldTypeBeApplied(seenValues []string) bool
	GetType() string
	GenerateFromTypeFunction(functionScaffold *ast.FuncDecl) *ast.FuncDecl
	GenerateToTypeFunction(functionScaffold *ast.FuncDecl) *ast.FuncDecl
}

CouldTypeBeApplied(seenValues []string) bool Receives an array of strings, and returns true if the type could be applied based on the given set of values.

GetType() Returns the type as a string, for example, time.Time for the time type.

GenerateFromTypeFunction(functionScaffold *ast.FuncDecl) *ast.FuncDecl and GenerateToTypeFunction(functionScaffold *ast.FuncDecl) *ast.FuncDecl receive a function with its name and header and return values set. The function only needs to generate the function body. To generate the ast-code it's advised to write the code first into a dummy function and use this amazing tool https://astextract.lu4p.xyz/ to convert it to ast-code.

Build-in TypeCheckers

NameTypeDescription
UUIDTypeCheckeruuid.UUIDChecks if values could be represented as uuid.UUID using github.com/google/uuid as a dependency
TimeTypeCheckertime.TimeChecks if values can be represented as time.Time uses github.com/araddon/dateparse to check for valid time types.
IntTypeCheckerintChecks if the given values can be represented as integers. Be careful with APIs that use the dot notation, to signal a float response, despite the values being valid integer ones. The information is lost during the process. Only the pure value plays a role.

The build-in time adjuster is a good starting point for your own Typeadjuster, and could be found here: typeDeterminers.go

JSON-Marshaller generation

If desired, custom marshall functions could be generated. If Typeadjusters are used, this is required!

UnMarshal

JSON2Go.GenerateJsonUnmarshall(file)

The Unmarshal generator honors the custom types generated by the Typeadjusters. On top of that, it comes with logic, which soft errors whenever it encounters an unknown field. In this case a AdditionalElementErrors is returned. This could be used, to update the structure on the fly. The error contains the name of the parsed object and the unknown elements as a map[element's name]json.RawMessage

type AdditionalElementsError struct {
	ParsedObj string
	Elements  map[string]json.RawMessage
}

The soft error is realized by joining all AdditionalElementErrors together. Only if an error not of the
AdditionalElementError type is observed, is the processing terminated, and the combined error is returned.

To check for an error on the caller side, use CheckForFirstErrorNotOfTypeT. To collect all AdditionalElementsErrors use GetAllErrorsOfType.

Example

Check for non AdditionalElementError

var rawJson []byte
var generatedStruct GeneratedStruct
err := json.Unmarshal(rawJson, &generatedStruct)
e := GeneratePackage.CheckForFirstErrorNotOfTypeT(GeneratePackage.AdditionalElementError{}, err)
if e != nil {
	//TODO handle error
}

Get slice of AdditionalElementErrors

elementErrors, err := GeneratePackage.GetAllErrorsOfType(GeneratePackage.AdditionalElementError{}, err)
if err != nil {
	return
} else {
	for _, elementError := range elementErrors {
		//TODO handle  AdditionalElementError
	}
}

Marshal

JSON2Go.GenerateJsonMarshall(file)

A marshal function is generated for each struct, that contains a custom type.

Combine files

When working with undocumented JSON-APIs it's essential to generate the datatype not from a single response. But instead use a wide set of responses, to be able to get the most complete data structure. To archive this, Json2Go comes with the ability to combine all equally named structs of two files into one file.

Let's say we're received the following to JSON-Responses from an API:

{
  "Name": "Nick",
  "Age": "99",
  "Food": "BBQ"
}
{
  "Name": "Peter",
  "Age": "22",
  "Gender": "male",
  "Origin": "US"
}
type RRR struct {
	Food	string	`json:"Food" json2go:"eyJzZWVuVmFsdWVzIjpbIkJCUSJdfQ=="`
	Gender	string	`json:"Gender" json2go:"eyJzZWVuVmFsdWVzIjpbIm1hbGUiXX0="`
	Origin	string	`json:"Origin" json2go:"eyJzZWVuVmFsdWVzIjpbIlVTIl19"`
	Name	string	`json:"Name" json2go:"eyJzZWVuVmFsdWVzIjpbIk5pY2siLCJQZXRlciJdfQ==" `
	Age	string	`json:"Age" json2go:"eyJzZWVuVmFsdWVzIjpbIjk5IiwiMjIiXX0=" `
}