Skip to content

JSON-LD Parsing and Formatting

JsonLDUtil

Knora provides a utility object called JsonLDUtil, which wraps the titanium-json-ld Java library, and parses JSON-LD text to a Knora data structure called JsonLDDocument. These classes provide commonly needed functionality for extracting and validating data from JSON-LD documents, as well as for constructing new documents.

Parsing JSON-LD

A route that expects a JSON-LD request must first parse the JSON-LD using JsonLDUtil . For example, this is how ValuesRouteV2 parses a JSON-LD request to create a value:

post {
            entity(as[String]) { jsonRequest =>
                requestContext => {
                    val requestDoc: JsonLDDocument = JsonLDUtil.parseJsonLD(jsonRequest)

The result is a JsonLDDocument in which all prefixes have been expanded to full IRIs, with an empty JSON-LD context.

The next step is to convert the JsonLDDocument to a request message that can be sent to the Knora responder that will handle the request.

val requestMessageFuture: Future[CreateValueRequestV2] = for {
                        requestingUser <- getUserADM(requestContext)
                        requestMessage: CreateValueRequestV2 <- CreateValueRequestV2.fromJsonLD(
                            requestDoc,
                            apiRequestID = UUID.randomUUID,
                            requestingUser = requestingUser,
                            responderManager = responderManager,
                            storeManager = storeManager,
                            settings = settings,
                            log = log
                        )
                    } yield requestMessage

This is done in a Future, because the processing of JSON-LD input could in itself involve sending messages to responders.

Each request message case class (in this case CreateValueRequestV2) has a companion object that implements the KnoraJsonLDRequestReaderV2 trait:

/**
  * A trait for objects that can generate case class instances based on JSON-LD input.
  *
  * @tparam C the type of the case class that can be generated.
  */
trait KnoraJsonLDRequestReaderV2[C] {
    /**
      * Converts JSON-LD input into a case class instance.
      *
      * @param jsonLDDocument   the JSON-LD input.
      * @param apiRequestID     the UUID of the API request.
      * @param requestingUser   the user making the request.
      * @param responderManager a reference to the responder manager.
      * @param storeManager     a reference to the store manager.
      * @param settings         the application settings.
      * @param log              a logging adapter.
      * @param timeout          a timeout for `ask` messages.
      * @param executionContext an execution context for futures.
      * @return a case class instance representing the input.
      */
    def fromJsonLD(jsonLDDocument: JsonLDDocument,
                   apiRequestID: UUID,
                   requestingUser: UserADM,
                   responderManager: ActorRef,
                   storeManager: ActorRef,
                   settings: KnoraSettingsImpl,
                   log: LoggingAdapter)(implicit timeout: Timeout, executionContext: ExecutionContext): Future[C]
}

This means that the companion object has a method fromJsonLD that takes a JsonLDDocument and returns an instance of the case class. The fromJsonLD method can use the functionality of the JsonLDDocument data structure for extracting and validating the content of the request. For example, JsonLDObject.requireStringWithValidation gets a required member of a JSON-LD object, and validates it using a function that is passed as an argument. Here is an example of getting and validating a SmartIri:

for {
      valueType: SmartIri <- Future(jsonLDObject.requireStringWithValidation(JsonLDConstants.TYPE, stringFormatter.toSmartIriWithErr))

The validation function (in this case stringFormatter.toSmartIriWithErr) has to take two arguments: a string to be validated, and a function that that throws an exception if the string is invalid. The return value of requireStringWithValidation is the return value of the validation function, which in this case is a SmartIri. If the string is invalid, requireStringWithValidation throws BadRequestException.

It is also possible to get and validate an optional JSON-LD object member:

val maybeDateValueHasStartEra: Option[DateEraV2] = jsonLDObject.maybeStringWithValidation(OntologyConstants.KnoraApiV2Complex.DateValueHasStartEra, DateEraV2.parse)

Here JsonLDObject.maybeStringWithValidation returns an Option that contains the return value of the validation function (DateEraV2.parse) if it was given, otherwise None.

Returning a JSON-LD Response

Each API response is represented by a message class that extends KnoraJsonLDResponseV2, which has a method toJsonLDDocument that specifies the target ontology schema. The implementation of this method constructs a JsonLDDocument, in which all object keys are full IRIs (no prefixes are used), but in which the JSON-LD context also specifies the prefixes that will be used when the document is returned to the client. The function JsonLDUtil.makeContext is a convenient way to construct the JSON-LD context.

Since toJsonLDDocument has to return an object that uses the specified ontology schema, the recommended design is to separate schema conversion as much as possible from JSON-LD generation. As a first step, schema conversion (or at the very least, the conversion of Knora type IRIs to the target schema) can be done via an implementation of KnoraReadV2:

/**
  * A trait for read wrappers that can convert themselves to external schemas.
  *
  * @tparam C the type of the read wrapper that extends this trait.
  */
trait KnoraReadV2[C <: KnoraReadV2[C]] {
    this: C =>
    def toOntologySchema(targetSchema: ApiV2Schema): C
}

This means that the response message class has the method toOntologySchema, which returns a copy of the same message, with Knora type IRIs (and perhaps other content) adjusted for the target schema. (See Smart IRIs on how to convert Knora type IRIs to the target schema.)

The response message class could then have a private method called generateJsonLD, which generates a JsonLDDocument that has the correct structure for the target schema, like this:

private def generateJsonLD(targetSchema: ApiV2Schema, settings: KnoraSettingsImpl, schemaOptions: Set[SchemaOption]): JsonLDDocument

This way, the implementation of toJsonLDDocument can call toOntologySchema, then construct a JsonLDDocument from the resulting object. For example:

    override def toJsonLDDocument(targetSchema: ApiV2Schema, settings: KnoraSettingsImpl, schemaOptions: Set[SchemaOption] = Set.empty): JsonLDDocument = {
        toOntologySchema(targetSchema).generateJsonLD(
            targetSchema = targetSchema,
            settings = settings,
            schemaOptions = schemaOptions
        )
    }

Selecting the Response Schema

Most routes complete by calling RouteUtilV2.runRdfRouteWithFuture, which calls the response message's toJsonLDDocument method. The runRdfRouteWithFuture function has a parameter that enables the route to select the schema that should be used in the response. It is up to each route to determine what the appropriate response schema should be. Some routes support only one response schema. Others allow the client to choose. To use the schema requested by the client, the route can call RouteUtilV2.getOntologySchema:

RouteUtilV2.runRdfRouteWithFuture(
    requestMessageF = requestMessageFuture,
    requestContext = requestContext,
    settings = settings,
    responderManager = responderManager,
    log = log,
    targetSchema = targetSchema,
    schemaOptions = schemaOptions
)

If the route only supports one schema, it can specify the schema directly instead:

RouteUtilV2.runRdfRouteWithFuture(
    requestMessageF = requestMessageFuture,
    requestContext = requestContext,
    settings = settings,
    responderManager = responderManager,
    log = log,
    targetSchema = ApiV2Complex,
    schemaOptions = RouteUtilV2.getSchemaOptions(requestContext)
)

Generating Other RDF Formats

RouteUtilV2.runRdfRouteWithFuture implements HTTP content negotiation. After determining the client's preferred format, it asks the KnoraResponseV2 to convert itself into that format. KnoraResponseV2 has an abstract format method, whose implementations select the most efficient conversion between the response message's internal representation (which could be JSON-LD or Turtle) and the requested format.


Last update: January 19, 2023