poly

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 21, 2023 License: MIT Imports: 5 Imported by: 0

README

go-poly

Build Status Go Report Card GoDoc License

go-poly is a GoLang library that provides functionality for marshalling and unmarshalling polymorphic JSON. It is designed to simplify the handling of JSON objects with varying structures in Go applications.

Features

  • Simplified marshalling and unmarshalling of polymorphic JSON
  • Support for custom types
  • Minimal dependencies and efficient performance
  • Flexible usage patterns
  • Preservation of original context, including indexes

Installation

To install go-poly, use the following command:

go get -u github.com/gburgyan/go-poly

Usage

Polymorphic JSON allows objects to have varying structures within a single collection due to the dynamic nature and flexibility of JSON data structures. This is useful when dealing with diverse data sources, evolving APIs, or maintaining backward compatibility. However, Go lacks native support for handling polymorphic JSON structures.

go-poly aims to address this limitation by enabling easy marshalling and unmarshalling of polymorphic JSONs without the need for custom functions.

Unmarshalling

Take this JSON for example that might define a residence:

[
  {
    "type": "location",
    "address": "123 Main"
  },
  {
    "type": "person",
    "name": "John",
    "occupation": "Teacher",
    "age": 35
  },
  {
    "type": "person",
    "name": "Mary",
    "occupation": "Programmer",
    "age": 33
  },
  {
    "type": "pet",
    "name": "Rover",
    "species": "dog"
  },
  {
    "type": "pet",
    "name": "Fluffy",
    "species": "cat"
  },
  {
    "type": "water",
    "provider": "Public City Water"
  }
]

Using go-poly you can marshal and unmarshal with a set of objects like this:

type Residence struct {
    Location Location      `poly:"location"`
    People   []Person      `poly:"person"`
    Pets     []Pet         `poly:"pet"`
    Water    *WaterService `poly:"water"`
}

func (r *Residence) UnmarshalJSON(rawJson []byte) error {
    return Unmarshal(rawJson, r)
}

func (r Residence) MarshalJSON() ([]byte, error) {
    return Marshall(r)
}

type Location struct {
    Address string `json:"address"`
}

type Person struct {
    Name       string `json:"name,omitempty"`
    Occupation string `json:"occupation,omitempty"`
    Age        int    `json:"age,omitempty"`
}

type Pet struct {
    Name    string `json:"name,omitempty"`
    Species string `json:"species,omitempty"`
}

type WaterService struct {
    Provider string `json:"provider,omitempty"`
}

You can then unmarshall the JSON into your object by:

var residence Residence
err := json.Unmarshal(input, &residence)

Since you've implemented the json.Unmarshaler interface, your function UnmarshalJSON will get called to handle the unmarshalling. This will happen even this is buried within a larger JSON document.

Alternately, you can call the polymorphic unmarshalling directly:

var residence Residence
err := poly.Unmarshal(input, &residence)

This library handles slices of objects by appending newly unmarshalled objects to the slice. For struct types or pointers to struct types, they are simply assigned. If multiple instances of a scalar type are unmarshalled, the last instance will overwrite earlier ones.

Type Lookups

The default implementation uses the GenericTypeLocator which looks for common type discriminators:

  • type
  • Type
  • @type
  • @Type

For custom implementations, provide a Type that implements the TypeLocator interface and pass it to UnmarshalCustom. During the unmarshalling process, the JSON will first be converted into a slice of your custom type. Subsequently, each instance in the slice will be used to determine the actual object type for unmarshalling. This approach offers flexibility, allowing your implementation to perform any necessary actions to identify the correct type. For example, if you need to examine multiple JSON fields to determine the concrete type, your custom implementation can handle that.

Finding the correct target field

The returned type name is used to figure out what field in the target object will get filled. If there is no poly tag on a field, the name of the field is used verbatim. If the field has a poly tag, then that is used to find the correct field.

Indexing

In cases where the order of elements in the JSON array is important, implement the IndexSettable interface for the types being deserialized.

After unmarshalling, the SetIndex(index int) function will be called with the zero-based index of the JSON array from which the object was unmarshalled.

Marshalling

As with unmarshalling, implementing the json.Marshaler interface will trigger the MarshalJSON function during the marshalling process. When calling json.Marshal, your function will handle marshalling, and the polymorphic JSON will be emitted.

bytes, err := json.Marshal(residence)

You can also manually call poly.Marshal without implementing any interface, but this method will not participate in the standard json.Marshal behavior.

bytes, err := poly.Marshal(residence)

If you only need to flatten your object instead, you can call poly.Flatten, which does all the marshalling work without the JSON transformation. It will return a slice of any which you can handle however you need.

Indexing

Similar to unmarshalling, the order of elements in the JSON array may be important during marshalling. To maintain the desired order, implement the IndexGettable interface for your object. The GetIndex() function will be called to determine the relative index.

If multiple elements return the same index, they will be grouped together, and the order in which they were encountered will be preserved within the same returned index.

Objects that do not implement the IndexGettable interface will be sorted to the end of the array.

Limitation

The library does not automatically emit a type field in the marshalled JSON. If you require a type discriminator in the JSON, include an appropriate field with the correct value in the objects being marshalled, as shown below:

type JsonObject struct {
    Type string `json:"type"`
    Value string
}

Without the Type field, or a similar field, the type will not be marshalled in the JSON.

License

go-poly is licensed under the MIT License.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultLocator = reflect.TypeOf(GenericTypeLocator{})

DefaultLocator is the type of the default TypeLocator that is used in the simpler implementation of the unmarshaller.

Functions

func Flatten

func Flatten(obj any) []any

Flatten takes an input object of any type and flattens the input object by extracting its fields and appending them to a slice. For fields of slice types, the function appends individual non-zero elements of the slice to the resulting flattened slice. The function also sorts the flattened slice based on the index of the elements if they implement the IndexGettable interface which can be used to control the ordering of the resultant JSON objects. If there are multiple objects that have the same index, they are sorted together with the internal order based on when they were first encountered. Any object that does not implement the IndexGettable interface will be sorted to the end using the same rules.

This does not marshal them into JSON, unlike Marshal, and can be used if there is a need to do any custom JSON serialization by your own code.

As in Marshal, the objects, when serialized to JSON by your code will need to have a field indicating the polymorphic type if you need that represented in the output JSON.

Parameters: - obj (any): The input object to be serialized. This can be a value or a pointer of any type.

Returns: - ([]any): A flattened representation of the input object with all the fields of the original object returned as a slice.

func Marshal added in v0.2.0

func Marshal(obj any) ([]byte, error)

Marshal takes an input object of any type and serializes it into a JSON byte array. The function flattens the input object by extracting its fields and appending them to a slice. For fields of slice types, the function appends individual non-zero elements of the slice to the resulting flattened slice. The function also sorts the flattened slice based on the index of the elements if they implement the IndexGettable interface which can be used to control the ordering of the resultant JSON objects. If there are multiple objects that have the same index, they are sorted together with the internal order based on when they were first encountered. Any object that does not implement the IndexGettable interface will be sorted to the end using the same rules.

Note that this only serialized the objects to JSON using the default JSON marshalling. This means that if you want to have a JSON value that can be used to determine the type of object when the JSON is being deserialized, the appropriate field must be present. This does not magically add any type resolution to the output JSON objects.

Parameters: - obj (any): The input object to be serialized. This can be a value or a pointer of any type.

Returns: - ([]byte, error): A JSON byte array representing the serialized input object, and an error if any occurs during the marshalling process.

The function is designed to be flexible and support a wide range of input types. The resulting JSON byte array is a representation of the input object's fields and their values, with nested structures being flattened and sorted based on the index provided by the IndexGettable interface. This function is useful for situations where a more compact or custom JSON representation is desired for complex data structures.

func Unmarshal added in v0.2.0

func Unmarshal(rawJson []byte, target any) error

Unmarshal is a convenience function that takes a raw JSON byte slice and a target any type variable, and unmarshalls the JSON into the target variable based on the default polymorphism rules. The target variable should be a struct with fields tagged with their respective polymorphic type names. The default polymorphism rules are defined by the DefaultLocator, which implements some common polymorphic type resolutions using "type", "@type", "Type", and "@Type" as the keys to determine the type of object based on the JSON. If your needs are different, you can define a custom TypeLocator which handles the type resolution in whatever way is needed for your application.

This function is a wrapper around UnmarshalCustom, using the DefaultLocator for type resolution. If an error occurs during unmarshalling, it returns an error.

Example usage:

	type Dog struct { ... }
	type Cat struct { ... }
	type Owner struct { ... }

	type Result struct {
	    Dogs  []Dog `poly:"dog"`
	    Cats  []Cat `poly:"cat"`
     	Owner Owner `poly:"owner"`
	}

	var result Result
	err := Unmarshal(jsonData, &result)

In this example, the Unmarshal function would unmarshall the JSON into the Result struct, populating the Dogs and Cats slices based on the polymorphic type names defined in the DefaultLocator struct.

func UnmarshalCustom added in v0.2.0

func UnmarshalCustom(rawJson []byte, target any, typeLocator reflect.Type) error

UnmarshalCustom takes a raw JSON byte slice, a target any type variable, and a typeLocator of type reflect.Type. It unmarshalls the JSON into the target variable based on the custom type polymorphism rules defined by the typeLocator. The target variable should be a struct with fields tagged with their respective polymorphic type names. The typeLocator should be a struct implementing the TypeLocator interface which returns a type name for the current object. If an error occurs during unmarshalling, it returns an error.

Example usage:

	type Dog struct { ... }
	type Cat struct { ... }
	type Owner struct { ... }

	type AnimalTypeLocator struct { ... }
	func (tl *AnimalTypeLocator) TypeName() string { ... }

	type Result struct {
	    Dogs  []Dog `poly:"dog"`
	    Cats  []Cat `poly:"cat"`
  	    Owner Owner `poly:"owner"`
	}

	var result Result
	err := UnmarshalCustom(jsonData, &result, reflect.Type(AnimalTypeLocator{})

In this example, the UnmarshalCustom function would unmarshal the JSON into the Result struct, populating the Dogs and Cats slices based on the polymorphic type names defined in the TypeLocator struct.

Types

type GenericTypeLocator

type GenericTypeLocator struct {
	Type       string `json:"type,omitempty"`
	TypeAt     string `json:"@type,omitempty"`
	TypeCaps   string `json:"Type,omitempty"`
	TypeAtCaps string `json:"@Type,omitempty"`
}

GenericTypeLocator provides a default implementation of the TypeLocator that handles common cases.

func (*GenericTypeLocator) TypeName

func (t *GenericTypeLocator) TypeName() string

TypeName returns the name of the generic type represented by the receiver.

type IndexGettable

type IndexGettable interface {
	GetIndex() int
}

type IndexSettable

type IndexSettable interface {
	// SetIndex is called with the zero-based index into the JSON sub-object.
	// In cases where there are sub-objects that cannot be unmarshalled, those
	// still count in the indexing.
	SetIndex(index int)
}

IndexSettable is an interface that you should implement if you need to know the index into the array of JSON sub-objects. This should be implemented by the types that are referred to by the TypeLocator.

type TypeLocator

type TypeLocator interface {
	// TypeName returns the name of the generic type needed to satisfy the
	// polymorphic unmarshalling.
	TypeName() string
}

TypeLocator needs to be implemented by whatever pre-deserializing type that is used to determine what is the actual type that we should create for each sub-object of whatever JSON array we're unmarshalling. Whatever `struct` you make that implements this should have the correct JSON members that will be used to determine which object to create to properly unmarshal the object.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL