Skip to content

Instantly share code, notes, and snippets.

@gamache
Last active November 6, 2021 10:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save gamache/e8e24eee5bd3f190de23 to your computer and use it in GitHub Desktop.
Save gamache/e8e24eee5bd3f190de23 to your computer and use it in GitHub Desktop.
Validating Data in Elixir with ExJsonSchema

priv/schema.json

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Events API Schema",

  "definitions": {

    "event_collection": {
      "type": "object",
      "required": ["events"],
      "properties": {
        "events": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/event"
          }
        }
      }
    },

    "event": {
      "type": "object",
      "required": ["name", "timestamp"],
      "properties": {
        "name": {
          "type": "string"
        },
        "timestamp": {
          "type": ["string", "integer"],
          "format": "date-time"
        },
        "attributes": {
          "type": "object"
        }
      }
    }
  }
}

json_schema.ex

defmodule JsonSchema do
  @moduledoc ~S"""
  A service which validates objects according to types defined
  in `schema.json`.
  """

  @doc ~S"""
  Validates an object by type.  Returns a list of {msg, [columns]} tuples
  describing any validation errors, or [] if validation succeeded.
  """
  def validate(server \\ :json_schema, object, type) do
    GenServer.call(server, {:validate, object, type})
  end

  @doc ~S"""
  Returns true if the object is valid according to the specified type,
  false otherwise.
  """
  def valid?(server \\ :json_schema, object, type) do
    [] == validate(server, object, type)
  end

  @doc ~S"""
  Converts the output of `validate/3` into a JSON-compatible structure,
  a list of error messages.
  """
  def errors_to_json(errors) do
    errors |> Enum.map(fn ({msg, _cols}) -> msg end)
  end


  use GenServer

  def init(_) do
    schema = File.read!(Application.app_dir(:myapp) <> "/priv/schema.json")
             |> Poison.decode!
             |> ExJsonSchema.Schema.resolve
    {:ok, schema}
  end

  def handle_call({:validate, object, type}, _from, schema) do
    errors = get_validation_errors(object, type, schema)
             |> transform_errors
    {:reply, errors, schema}
  end

  defp get_validation_errors(object, type, schema) do
    type_string = type |> to_string
    type_schema = schema.schema["definitions"][type_string]

    not_a_struct = case object do
      %{__struct__: _} -> Map.from_struct(object)
      _ -> object
    end

    string_keyed_object = ensure_key_strings(not_a_struct)

    ## validate throws a BadMapError on certain kinds of invalid
    ## input; absorb it (TODO fix ExJsonSchema upstream)
    try do
      ExJsonSchema.Validator.validate(schema, type_schema, string_keyed_object)
    rescue
      _ -> [{"Failed validation", []}]
    end
  end

  @doc ~S"""
  Makes sure that all the keys in the map are strings and not atoms.
  Works on nested data structures.
  """
  defp ensure_key_strings(x) do
    cond do
      is_map x ->
        Enum.reduce x, %{}, fn({k,v}, acc) ->
          Map.put acc, to_string(k), ensure_key_strings(v)
        end
      is_list x ->
        Enum.map(x, fn (v) -> ensure_key_strings(v) end)
      true ->
        x
    end
  end

end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment