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.Labels 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.