With this extension module of graph-core
, you are able to encode and decode Graphs to and from a
JSON text in an efficient way.
Codecs are implemented in terms of jsoniter-scala.
Your encoded Graphs will be laid out like
{ "nodes": [ <a JSON object per node> ], "edges": [ <a JSON object per edge> ] }
While nodes will be encoded without any transformation, for edge codecs you can choose between the following two schemas:
N
is of a primitive-like type
including primitive types, String
, or any other type with a fairly small JSON representation.
To be able to encode your Graph or decode your JSON to a Graph, you need to instantiate a Graph codec. Graph codecs vary with the coding schema and the edge type argument used for your Graph.
Using this schema, edge ends are represented by fully encoded nodes. This schema is your friend whenever your node type argument is of a primitive-like type. Let's look at some examples:
Given
type G = Graph[Int, DiEdge[Int]]
you get the codec like
import scalax.collection.io.jsoniter.GraphCodec given nodeCodec: JsonValueCodec[Int] = JsonCodecMaker.make given edgeCodec: JsonValueCodec[DiEdge[Int]] = JsonCodecMaker.make given graphCodec: JsonValueCodec[G] = GraphCodec.withEmbeddedNodes( Graph.from(_, _)(_), yourGraphConfig, yourNullHandling )
Given
type People = Graph[Person, Relation]
where Person
is a case class and Relation
is an ADT of edges with constructors like
Relatives
, Friends
, or Neighbors
,
you can create the codec in the very same way
import scalax.collection.io.jsoniter.GraphCodec given nodeCodec: JsonValueCodec[Person] = JsonCodecMaker.make given edgeCodec: JsonValueCodec[Relation] = JsonCodecMaker.make given graphCodec: JsonValueCodec[People] = GraphCodec.withEmbeddedNodes( Graph.from(_, _)(_), yourGraphConfig, yourNullHandling )
Further, specific handling of non-ADT edge type arguments is provided since this is not covered by jsoniter-scala. Given
type G = Graph[String, AnyEdge[String]]
where your Graph contains the concrete edge classes DiEdge
and UnDiEdge
,
you can create the codec like
import scalax.collection.io.jsoniter.GraphCodec given nodeCodec: JsonValueCodec[String] = JsonCodecMaker.make given anyEdgeCodec: JsonValueCodec[AnyEdge[String]] = given unDiEdgeCodec: JsonValueCodec[UnDiEdge[String]] = JsonCodecMaker.make given diEdgeCodec: JsonValueCodec[DiEdge[String]] = JsonCodecMaker.make EdgeCodec.makePolymorphicWithEmbeddedNodes[AnyEdge[String], (UnDiEdge[String], DiEdge[String])]() given graphCodec: JsonValueCodec[G] = GraphCodec.withEmbeddedNodes( Graph.from(_, _)(_), yourGraphConfig, yourNullHandling )
For complete examples refer to EmbeddedNodesSpec.scala.
With this schema, edge ends are encoded to node IDs that are obtained by invoking your ID function. This schema is your friend whenever you are concerned about the size and readability of the encoded edges.
The replacement of edge ends by a corresponding node ID is implemented such that edges get transformed into constructor instances of the nonlabeled.WithNodeReferences respectively labeled.WithNodeReferences ADT. These instances will then be encoded to JSON.
Let's look at an example for a Graph with non-labeled and another example for a Graph with labeled edges:
To start with non-labeled edges, given
case class Airport(code: String, nameByLanguage: Map[String, String]) sealed trait Relation extends AnyHyperEdge[Airport] case class NonStop(airport1: Airport, airport2: Airport) extends AbstractUnDiEdge(airport1, airport2) with Relation case class Partnership(partners: Several[Airport]) extends AbstractHyperEdge(partners) with Relation type Airports = Graph[Airport, Relation] object Airports extends TypedGraphFactory[Airport, Relation]
here is how to create a codec that encodes edge ends by the airport code
which is unique over airports:
import scalax.collection.io.jsoniter.nonlabeled.* import AnyHyperEdgeWithNodeReferences.compactClassNames given nodeCodec: JsonValueCodec[Airport] = JsonCodecMaker.make given idCodec: JsonValueCodec[String] = JsonCodecMaker.make given edgeCodec: JsonValueCodec[AnyHyperEdgeWithNodeReferences[String]] = JsonCodecMaker.make(compactClassNames) given graphCodec: JsonValueCodec[Airports] = GraphCodec.withNodeReferences( _.code, HyperEdgeWithNodeReferences.apply, Graph.from(_, _)(_), yourGraphConfig, yourNullHandling, edgeFactory = Some { case ("NonStop", a1, a2) => NonStop(a1, a2) }, hyperEdgeFactory = Some { case ("Partnership", ends) => Partnership(ends) } )
As to
nonlabeled
package because edges are not labeled."type": "UnDiEdgeWithNodeReferences"
gets "type": "UnDiR"
.String
reflecting the type of code
returned by the ID function.AnyHyperEdgeWithNodeReferences[String]
because the Graph contains both undirected edges and hyperedges.AnyHyperEdgeWithNodeReferences
that consumes any edge type having the superclass HyperEdge
."edgeT": "NonStop"
,
we create an edge of type NonStop
.
Note that at invocation, the codec has transformed the ID back to Airport
by looking up the airport in the decoded nodes by code
.
"edgeT": "Partnership"
,
we create a hyperedge of type Partnership
.
Importantly, you need to cover all participating edge types by the passed edge factories.
Otherwise, decoding throws an exception.
Second, given a related use case extended by edge labels
case class Airport(code: String, nameByLanguage: Map[String, String]) sealed trait Relation extends AnyDiEdge[Airport] case class NonStop(from: Airport, to: Airport, airline: String) extends AbstractDiEdge(from, to) with MultiEdge with Relation: override def extendKeyBy: OneOrMore[String] = OneOrMore(airline) case class Connecting(from: Airport, to: Airport, via: String, airline1: String, airline2: String) extends AbstractDiEdge(from, to) with MultiEdge with Relation: override def extendKeyBy: OneOrMore[(String, String, String)] = OneOrMore((via, airline1, airline2)) type Airports = Graph[Airport, Relation] object Airports extends TypedGraphFactory[Airport, Relation]
you can get the Graph codec including labels like
import scalax.collection.io.jsoniter.labeled.* import AnyEdgeWithNodeReferences.compactClassNames given nodeCodec: JsonValueCodec[Airport] = JsonCodecMaker.make given idCodec: JsonValueCodec[String] = JsonCodecMaker.make sealed trait Label case class NonStopLabel(airline: String) extends Label case class ConnectingLabel(via: String, airline1: String, airline2: String) extends Label given labelCodec: JsonValueCodec[Label] = JsonCodecMaker.make given edgeCodec: JsonValueCodec[DiEdgeWithNodeReferences[String, Label]] = JsonCodecMaker.make(compactClassNames) given graphCodec: JsonValueCodec[Airports] = GraphCodec.withNodeReferences( _.code, { case NonStop(_, _, airline) => NonStopLabel(airline) case Connecting(_, _, via, a1, a2) => ConnectingLabel(via, a1, a2) }, DiEdgeWithNodeReferences.apply, factory = Graph.from(_, _)(_), yourGraphConfig, yourNullHandling, edgeFactory = Some { case ("NonStop", from, to, NonStopLabel(airline)) => NonStop(from, to, airline) case ("Connecting", from, to, ConnectingLabel(via, a1, a2)) => Connecting(from, to, via, a1, a2) } )
Note the differences to the non-labeled use case
labeled
package to be able to deal with edge labels.Label
s will also be encoded into DiEdgeWithNodeReferences
.toLabel
argument defines what to encode as the Label
part of edges.For more and complete examples refer to
In the above Library example, by the way, nodes are modeled as an ADT, so the ID function needs to deal with every case.
With your Graph codec in the implicit scope, you are ready to encode your Graph by using syntactic sugar like
import scalax.collection.io.jsoniter.toJson val json: String = graph.toJson
With your Graph codec in the implicit scope, you can decode your JSON to a Graph by simply
import scalax.collection.io.jsoniter.toGraph val graph: G = json.toGraph[G]
where G
stands for your Graph type.