Best way for validation outside of changeset?

I’m trying to wrap my head around the precise way to do data validation. Should all the validation be done inside the changeset (model) or is it appropriate to have some within the controller as well?

As an example, I will use file uploading:

defp validate_thumbnail(changeset, thumbnail) do
		case changeset.valid? do
			true ->
				if !is_binary(thumbnail) do
					if File.exists? thumbnail.path do
						{type, width, height, _} = ExImageInfo.info(File.read!(thumbnail.path))

						extension = Path.extname(thumbnail.filename)
						case File.cp(thumbnail.path, "media/thumbnail#{extension}") do
							:ok -> changeset
							{:error, reason} -> add_error(changeset, :thumbnail, "file could not be uploaded")
						end
					else
						add_error(changeset, :thumbnail, "is not valid")
					end
				else
					add_error(changeset, :thumbnail, "is not valid")
				end
			_ ->
				changeset
		end
	end

This doesn’t seem like something that should belong in changeset validation but on the other hand, if I do this within the controller, I need to check for errors within the controller logic and changeset logic.

My usual practice is to do all validation in the schema’s changeset function, unless it cannot be done in a pure function. So yes I would do File.exists? type stuff outside of the changeset functions and then add errors as well. I wouldn’t put this in the controller though, but in a context module or some other module dedicated to the purpose of managing the application’s use of the file system.

2 Likes

Each validation has its own place, depending on the context for example, you might want to add file type and size validation on the controller (calling a module that does this) because you don’t want that thing to enter on your sacred system, but at the same time you can do name validation on your schema, if the name is large than possible or contains illegal characters. Here is a nice reference

1 Like

Just some additional tips that you might consider:

  1. Use head functions to validate if changeset is valid:
defp validate_thumbnail(%{valid?: false} = changeset, thumbnail), do: changeset
defp validate_thumbnail(changeset, thumbnail) do
......
end
  1. You might want to consider separating in different validation functions the following checks: a) !is_binary() b) File.exists?

This will help you reuse this validations for other use cases

Best regards,

1 Like

Thank you for the responses, that clears up a lot of what I was thinking. I’ll be sure to endeavour in implementing the things you mentioned. Much appreciated.