Skip to content

Instantly share code, notes, and snippets.

@drewblas
Forked from gamache/validating.md
Last active March 30, 2016 14:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drewblas/b8f81c9fdba1824ec2939c5847871051 to your computer and use it in GitHub Desktop.
Save drewblas/b8f81c9fdba1824ec2939c5847871051 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

  ### FROM DREW:
  ### This is a more minor quible, but I thought I'd throw it in to try and help:
  #
  # Calling validate should not be done from inside the GenServer
  # Doing so means that there is essentially a single-line queue where ALL
  # validate calls are passing through this single synchronous mailbox.
  # So if you've got 1,000 phoenix connections/processes all trying to validate
  # input, they will wait one by one to validate.
  #
  # There's no reason for this, as the only thing the GenServer should be responsible
  # for is holding the state (the resolved schema).  Validating doesn't CHANGE the state.
  # So a normal module function can fetch the resolved schema from the GenServer
  # and use it in the validate call.
  #
  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
      ### FROM DREW:
      ### This call is a big problem:
      # type_schema is a %{} (Plain map).  It's *NOT* a completely resolved schema (it's no longer a %ExJsonSchema.Schema.Root{}
      # This means that it will match on https://github.com/jonasschmidt/ex_json_schema/blob/238f565a62acaf6e304c66c3cca4d80d2ef43d22/lib/ex_json_schema/validator.ex#L23-L25
      # and RE-CALL .resolve every time.   Exactly what you were trying to avoid
      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