diff --git a/build.gradle b/build.gradle
index 173b0b422c1257117b94fb76064efb69cd798ccc..f8b63ac9c17c649e12994d46ef30fa4191d0cc98 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,6 @@ def vCargo = '1.4.18'
 def vSlf4j = '1.7.21'
 def vLog4j = '1.2.17'
 def vJunit = '4.12'
-// def vSaxon = '9.7.0-7'
 def vSaxon = '9.1.0.8'
 def vGroovy = '2.4.6'
 def vJython = '2.7.0'
@@ -50,6 +49,7 @@ apply plugin: 'groovy'
 apply plugin: 'java'
 apply plugin: 'war'
 apply plugin: 'maven'
+apply plugin: 'jacoco'
 apply plugin: 'maven-publish'
 apply plugin: 'com.bmuschko.tomcat'
 apply plugin: 'com.bmuschko.cargo'
@@ -167,6 +167,17 @@ war {
                 'Implementation-Version': version
     }
 }
+jacoco {
+    toolVersion = "0.7.6.201602180812"
+}
+
+jacocoTestReport {
+    reports {
+        xml.enabled false
+        csv.enabled false
+        html.destination "${buildDir}/jacocoHtml"
+    }
+}
 
 task sourceJar(type: Jar, dependsOn: classes) {
     from sourceSets.main.allSource
diff --git a/src/main/java/org/nrg/xapi/rest/ApiException.java b/src/main/java/org/nrg/xapi/exceptions/ApiException.java
similarity index 87%
rename from src/main/java/org/nrg/xapi/rest/ApiException.java
rename to src/main/java/org/nrg/xapi/exceptions/ApiException.java
index f6e34b409067d47062917836cef519f9905cc9e9..2664e319cfc2611054a479bbbe6638907563b5c4 100644
--- a/src/main/java/org/nrg/xapi/rest/ApiException.java
+++ b/src/main/java/org/nrg/xapi/exceptions/ApiException.java
@@ -1,4 +1,4 @@
-package org.nrg.xapi.rest;
+package org.nrg.xapi.exceptions;
 
 public class ApiException extends Exception {
     public ApiException(int code, String msg) {
diff --git a/src/main/java/org/nrg/xapi/exceptions/DataFormatException.java b/src/main/java/org/nrg/xapi/exceptions/DataFormatException.java
new file mode 100644
index 0000000000000000000000000000000000000000..8af6ada60de5a0e9ffe9fa87aaa7876371080194
--- /dev/null
+++ b/src/main/java/org/nrg/xapi/exceptions/DataFormatException.java
@@ -0,0 +1,91 @@
+package org.nrg.xapi.exceptions;
+
+import com.google.common.base.Joiner;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@ResponseStatus(HttpStatus.BAD_REQUEST)
+public class DataFormatException extends ApiException {
+    private final List<String>        _missing = new ArrayList<>();
+    private final List<String>        _unknown = new ArrayList<>();
+    private final Map<String, String> _invalid = new HashMap<>();
+
+    public DataFormatException() {
+        this("There was an error with the submitted data");
+    }
+
+    public DataFormatException(final String message) {
+        super(HttpStatus.BAD_REQUEST.value(), message);
+    }
+
+    public List<String> getMissingFields() {
+        return _missing;
+    }
+
+    public void setMissing(final List<String> missing) {
+        _missing.clear();
+        _missing.addAll(missing);
+    }
+
+    public void addMissing(final String missing) {
+        _missing.add(missing);
+    }
+
+    public List<String> getUnknownFields() {
+        return _unknown;
+    }
+
+    public void setUnknown(final List<String> unknown) {
+        _unknown.clear();
+        _unknown.addAll(unknown);
+    }
+
+    public void addUnknown(final String unknown) {
+        _unknown.add(unknown);
+    }
+
+    public Map<String, String> getInvalidFields() {
+        return _invalid;
+    }
+
+    public void setInvalid(final Map<String, String> invalid) {
+        _invalid.clear();
+        _invalid.putAll(invalid);
+    }
+
+    public void addInvalid(final String invalid) {
+        _invalid.put(invalid, "Invalid " + invalid + " format");
+    }
+
+    public void addInvalid(final String invalid, final String message) {
+        _invalid.put(invalid, message);
+    }
+
+    public boolean hasDataFormatErrors() {
+        return !_missing.isEmpty() && !_unknown.isEmpty() && !_invalid.isEmpty();
+    }
+
+    @Override
+    public String getMessage() {
+        final StringBuilder buffer = new StringBuilder(super.getMessage());
+        buffer.append("\n");
+        if (_missing.size() > 0) {
+            buffer.append(" * Missing fields: ").append(Joiner.on(", ").join(_missing)).append("\n");
+        }
+        if (_unknown.size() > 0) {
+            buffer.append(" * Unknown fields: ").append(Joiner.on(", ").join(_unknown)).append("\n");
+        }
+        if (_invalid.size() > 0) {
+            buffer.append(" * Invalid fields:\n");
+            for (final String invalid : _invalid.keySet()) {
+                buffer.append("    - ").append(invalid).append(": ").append(_invalid.get(invalid)).append("\n");
+            }
+        }
+        return buffer.toString();
+    }
+}
diff --git a/src/main/java/org/nrg/xapi/exceptions/NotFoundException.java b/src/main/java/org/nrg/xapi/exceptions/NotFoundException.java
new file mode 100644
index 0000000000000000000000000000000000000000..4315274b6d53163a4bb41d932a80cc873e1f1f46
--- /dev/null
+++ b/src/main/java/org/nrg/xapi/exceptions/NotFoundException.java
@@ -0,0 +1,11 @@
+package org.nrg.xapi.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.NOT_FOUND)
+public class NotFoundException extends ApiException {
+    public NotFoundException(String msg) {
+        super(HttpStatus.NOT_FOUND.value(), msg);
+    }
+}
diff --git a/src/main/java/org/nrg/xapi/exceptions/ResourceAlreadyExistsException.java b/src/main/java/org/nrg/xapi/exceptions/ResourceAlreadyExistsException.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd39f298a5f4a35f335fa9707487fd534b4bdc93
--- /dev/null
+++ b/src/main/java/org/nrg/xapi/exceptions/ResourceAlreadyExistsException.java
@@ -0,0 +1,11 @@
+package org.nrg.xapi.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.CONFLICT)
+public class ResourceAlreadyExistsException extends ApiException {
+    public ResourceAlreadyExistsException(final String type, final String name) {
+        super(HttpStatus.CONFLICT.value(), "The resource of type " + type + " with the name " + name + " already exists.");
+    }
+}
diff --git a/src/main/java/org/nrg/xapi/model/users/User.java b/src/main/java/org/nrg/xapi/model/users/User.java
index b8b7574e10009c1a6c512fa5433318b328596e8f..9e0f751ed11fd756d51a3a913cac1523e6f46cb2 100644
--- a/src/main/java/org/nrg/xapi/model/users/User.java
+++ b/src/main/java/org/nrg/xapi/model/users/User.java
@@ -1,6 +1,7 @@
 package org.nrg.xapi.model.users;
 
 import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import org.nrg.xdat.entities.UserAuthI;
@@ -106,7 +107,7 @@ public class User {
      * Whether the user is a site administrator.
      **/
     @ApiModelProperty(value = "Whether the user is a site administrator.")
-    public boolean isAdmin() {
+    public Boolean isAdmin() {
         return _isAdmin;
     }
 
@@ -118,7 +119,7 @@ public class User {
      * Whether the user is enabled.
      **/
     @ApiModelProperty(value = "Whether the user is enabled.")
-    public boolean isEnabled() {
+    public Boolean isEnabled() {
         return _isEnabled;
     }
 
@@ -130,7 +131,7 @@ public class User {
      * Whether the user is verified.
      **/
     @ApiModelProperty(value = "Whether the user is verified.")
-    public boolean isVerified() {
+    public Boolean isVerified() {
         return _isVerified;
     }
 
@@ -142,7 +143,7 @@ public class User {
      * The user's encrypted password.
      **/
     @ApiModelProperty(value = "The user's encrypted password.")
-    @JsonIgnore
+    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
     public String getPassword() {
         return _password;
     }
@@ -155,7 +156,7 @@ public class User {
      * The _salt used to encrypt the user's _password.
      **/
     @ApiModelProperty(value = "The salt used to encrypt the user's password.")
-    @JsonIgnore
+    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
     public String getSalt() {
         return _salt;
     }
@@ -222,7 +223,7 @@ public class User {
     private String    _salt;
     private Date      _lastModified;
     private UserAuthI _authorization;
-    private boolean   _isAdmin;
-    private boolean   _isEnabled;
-    private boolean   _isVerified;
+    private Boolean   _isAdmin;
+    private Boolean   _isEnabled;
+    private Boolean   _isVerified;
 }
diff --git a/src/main/java/org/nrg/xapi/rest/NotFoundException.java b/src/main/java/org/nrg/xapi/rest/NotFoundException.java
deleted file mode 100644
index b42ca7024df3855805ae2ad47162d845ce074132..0000000000000000000000000000000000000000
--- a/src/main/java/org/nrg/xapi/rest/NotFoundException.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.nrg.xapi.rest;
-
-public class NotFoundException extends ApiException {
-    public NotFoundException(int code, String msg) {
-        super(code, msg);
-    }
-}
diff --git a/src/main/java/org/nrg/xapi/rest/XapiRestControllerAdvice.java b/src/main/java/org/nrg/xapi/rest/XapiRestControllerAdvice.java
index 07a3712cb27ee04f502ce0c8d5f5316eb420532d..5775a149fa8326d3a55079359115960d9dbc69aa 100644
--- a/src/main/java/org/nrg/xapi/rest/XapiRestControllerAdvice.java
+++ b/src/main/java/org/nrg/xapi/rest/XapiRestControllerAdvice.java
@@ -4,6 +4,8 @@ import org.nrg.dcm.exceptions.EnabledDICOMReceiverWithDuplicatePortException;
 import org.nrg.framework.exceptions.NrgServiceError;
 import org.nrg.framework.exceptions.NrgServiceException;
 import org.nrg.framework.exceptions.NrgServiceRuntimeException;
+import org.nrg.xapi.exceptions.DataFormatException;
+import org.nrg.xapi.exceptions.ResourceAlreadyExistsException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.core.annotation.AnnotationUtils;
@@ -26,6 +28,16 @@ public class XapiRestControllerAdvice {
         return handleException(request, exception.getMessage());
     }
 
+    @ExceptionHandler(DataFormatException.class)
+    public ModelAndView handleDataFormatException(final HttpServletRequest request, final DataFormatException exception) {
+        return handleException(request, exception.getMessage(), exception);
+    }
+
+    @ExceptionHandler(ResourceAlreadyExistsException.class)
+    public ModelAndView handleDataFormatException(final HttpServletRequest request, final ResourceAlreadyExistsException exception) {
+        return handleException(request, exception.getMessage(), exception);
+    }
+
     @ExceptionHandler(NrgServiceException.class)
     public ModelAndView handleNrgServiceException(final HttpServletRequest request, final NrgServiceException exception) {
         return handleException(HttpStatus.CONFLICT, request, "An NRG service error occurred.", exception);
diff --git a/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java b/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java
index ece0cd4d31679efa9b193628314fdc9ee0453b3c..140d294520db83f57629a000a5b4213bdf13c9a3 100644
--- a/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java
+++ b/src/main/java/org/nrg/xapi/rest/dicomscp/DicomSCPApi.java
@@ -7,7 +7,7 @@ import org.nrg.dcm.exceptions.EnabledDICOMReceiverWithDuplicatePortException;
 import org.nrg.dcm.preferences.DicomSCPInstance;
 import org.nrg.framework.annotations.XapiRestController;
 import org.nrg.framework.exceptions.NrgServiceException;
-import org.nrg.xapi.rest.NotFoundException;
+import org.nrg.xapi.exceptions.NotFoundException;
 import org.nrg.xdat.rest.AbstractXapiRestController;
 import org.nrg.xdat.security.services.RoleHolder;
 import org.nrg.xdat.security.services.UserManagementServiceI;
diff --git a/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java b/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java
index b650d8d041644e8fe8c2071e1ddb7462e5f51376..5dd40e81388d8b11798e2da8ff45dd64638ba25f 100644
--- a/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java
+++ b/src/main/java/org/nrg/xapi/rest/theme/ThemeApi.java
@@ -14,7 +14,7 @@ package org.nrg.xapi.rest.theme;
 import io.swagger.annotations.*;
 import org.apache.commons.io.FileUtils;
 import org.nrg.framework.annotations.XapiRestController;
-import org.nrg.xapi.rest.NotFoundException;
+import org.nrg.xapi.exceptions.NotFoundException;
 import org.nrg.xdat.entities.ThemeConfig;
 import org.nrg.xdat.rest.AbstractXapiRestController;
 import org.nrg.xdat.security.services.RoleHolder;
diff --git a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java
index 118075f8dbe39e8008f632ece0848c1d7936e57e..3e687d89e896ca78dfbfad373da52ea82cc2ca46 100644
--- a/src/main/java/org/nrg/xapi/rest/users/UsersApi.java
+++ b/src/main/java/org/nrg/xapi/rest/users/UsersApi.java
@@ -7,8 +7,11 @@ import org.jetbrains.annotations.Nullable;
 import org.nrg.framework.annotations.XapiRestController;
 import org.nrg.framework.exceptions.NrgServiceError;
 import org.nrg.framework.exceptions.NrgServiceRuntimeException;
+import org.nrg.framework.utilities.Patterns;
+import org.nrg.xapi.exceptions.DataFormatException;
+import org.nrg.xapi.exceptions.ResourceAlreadyExistsException;
 import org.nrg.xapi.model.users.User;
-import org.nrg.xapi.rest.NotFoundException;
+import org.nrg.xapi.exceptions.NotFoundException;
 import org.nrg.xdat.XDAT;
 import org.nrg.xdat.preferences.SiteConfigPreferences;
 import org.nrg.xdat.rest.AbstractXapiRestController;
@@ -194,70 +197,113 @@ public class UsersApi extends AbstractXapiRestController {
         }
     }
 
-    @ApiOperation(value = "Creates or updates the user object with the specified username.", notes = "Returns the updated serialized user object with the specified username.", response = User.class)
-    @ApiResponses({@ApiResponse(code = 200, message = "User successfully created or updated."),
+    @ApiOperation(value = "Updates the user object with the specified username.", notes = "Returns the updated serialized user object with the specified username.", response = User.class)
+    @ApiResponses({@ApiResponse(code = 201, message = "User successfully created."),
+                   @ApiResponse(code = 400, message = "The submitted data was invalid."),
                    @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
-                   @ApiResponse(code = 403, message = "Not authorized to create or update this user."),
+                   @ApiResponse(code = 403, message = "Not authorized to update this user."),
+                   @ApiResponse(code = 500, message = "An unexpected error occurred.")})
+    @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST)
+    public ResponseEntity<User> createUser(@RequestBody final User model) throws NotFoundException, PasswordComplexityException, DataFormatException, UserInitException, ResourceAlreadyExistsException {
+        final HttpStatus status = isPermitted();
+        if (status != null) {
+            return new ResponseEntity<>(status);
+        }
+
+        validateUser(model);
+
+        final UserI user = getUserManagementService().createUser();
+
+        if (user == null) {
+            throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Failed to create a user object for user " + model.getUsername());
+        }
+
+        user.setLogin(model.getUsername());
+        user.setFirstname(model.getFirstName());
+        user.setLastname(model.getLastName());
+        user.setEmail(model.getEmail());
+        if (model.isEnabled() != null) {
+            user.setEnabled(model.isEnabled());
+        }
+        if (model.isVerified()) {
+            user.setVerified(model.isVerified());
+        }
+        user.setPassword(model.getPassword());
+        user.setAuthorization(model.getAuthorization());
+
+        fixPassword(user);
+
+        try {
+            getUserManagementService().save(user, getSessionUser(), false, new EventDetails(EventUtils.CATEGORY.DATA, EventUtils.TYPE.WEB_SERVICE, Event.Added, "Requested by user " + getSessionUser().getUsername(), "Created new user " + user.getUsername() + " through XAPI user management API."));
+            return new ResponseEntity<>(new User(user), HttpStatus.CREATED);
+        } catch (Exception e) {
+            _log.error("Error occurred modifying user " + user.getLogin());
+        }
+        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
+    }
+
+    @ApiOperation(value = "Updates the user object with the specified username.", notes = "Returns the updated serialized user object with the specified username.", response = User.class)
+    @ApiResponses({@ApiResponse(code = 200, message = "User successfully updated."),
+                   @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."),
+                   @ApiResponse(code = 403, message = "Not authorized to update this user."),
                    @ApiResponse(code = 404, message = "User not found."),
                    @ApiResponse(code = 500, message = "An unexpected error occurred.")})
     @RequestMapping(value = "{username}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.PUT)
-    public ResponseEntity<User> usersIdPut(@ApiParam(value = "The username of the user to create or update.", required = true) @PathVariable("username") String username, @RequestBody User model) throws NotFoundException, PasswordComplexityException {
-        HttpStatus status = isPermitted(username);
+    public ResponseEntity<User> updateUser(@ApiParam(value = "The username of the user to create or update.", required = true) @PathVariable("username") final String username, @RequestBody final User model) throws NotFoundException, PasswordComplexityException, UserInitException {
+        final HttpStatus status = isPermitted(username);
         if (status != null) {
             return new ResponseEntity<>(status);
         }
-        UserI user;
+
+        final UserI user;
         try {
             user = getUserManagementService().getUser(username);
-        } catch (Exception e) {
-            //Create new User
-            user = getUserManagementService().createUser();
-            user.setLogin(username);
+        } catch (UserNotFoundException e) {
+            throw new NotFoundException("User with username " + username + " was not found.");
         }
+
         if (user == null) {
-            throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Failed to retrieve or create a user object for user " + username);
+            throw new NrgServiceRuntimeException(NrgServiceError.Unknown, "Failed to retrieve user object for user " + username);
         }
-        if ((StringUtils.isNotBlank(model.getFirstName())) && (!StringUtils.equals(model.getFirstName(), user.getFirstname()))) {
+
+        if ((StringUtils.isNotBlank(model.getUsername())) && (!StringUtils.equals(user.getUsername(), model.getUsername()))) {
+            user.setLogin(model.getUsername());
+        }
+        if ((StringUtils.isNotBlank(model.getFirstName())) && (!StringUtils.equals(user.getFirstname(), model.getFirstName()))) {
             user.setFirstname(model.getFirstName());
         }
-        if ((StringUtils.isNotBlank(model.getLastName())) && (!StringUtils.equals(model.getLastName(), user.getLastname()))) {
+        if ((StringUtils.isNotBlank(model.getLastName())) && (!StringUtils.equals(user.getLastname(), model.getLastName()))) {
             user.setLastname(model.getLastName());
         }
-        if ((StringUtils.isNotBlank(model.getEmail())) && (!StringUtils.equals(model.getEmail(), user.getEmail()))) {
+        if ((StringUtils.isNotBlank(model.getEmail())) && (!StringUtils.equals(user.getEmail(), model.getEmail()))) {
             user.setEmail(model.getEmail());
         }
-        if (model.isEnabled() != user.isEnabled()) {
+        if ((StringUtils.isNotBlank(model.getPassword())) && (!StringUtils.equals(user.getPassword(), model.getPassword()))) {
+            user.setPassword(model.getPassword());
+            fixPassword(user);
+        }
+        if (model.getAuthorization() != null && !model.getAuthorization().equals(user.getAuthorization())) {
+            user.setAuthorization(model.getAuthorization());
+        }
+        if (model.isEnabled() != null) {
             user.setEnabled(model.isEnabled());
-            if (!model.isEnabled()) {
-                //When a user is disabled, deactivate all their AliasTokens
-                try {
-                    XDAT.getContextService().getBean(AliasTokenService.class).deactivateAllTokensForUser(user.getLogin());
-                } catch (Exception e) {
-                    _log.error("", e);
-                }
-            }
         }
-        if (model.isVerified() != user.isVerified()) {
+        if (model.isVerified() != null) {
             user.setVerified(model.isVerified());
         }
 
-        final String password;
-        if (StringUtils.isNotBlank(model.getPassword())) {
-            password = model.getPassword();
-            if (!_passwordValidator.isValid(password, user)) {
-                throw new PasswordComplexityException(_passwordValidator.getMessage());
+        if (!user.isEnabled()) {
+            //When a user is disabled, deactivate all their AliasTokens
+            try {
+                XDAT.getContextService().getBean(AliasTokenService.class).deactivateAllTokensForUser(user.getLogin());
+            } catch (Exception e) {
+                _log.error("", e);
             }
-        } else {
-            password = RandomStringUtils.randomAscii(32);
         }
-        final String salt = Users.createNewSalt();
-        user.setPassword(new ShaPasswordEncoder(256).encodePassword(password, salt));
-        user.setPrimaryPassword_encrypt(true);
-        user.setSalt(salt);
 
         try {
             getUserManagementService().save(user, getSessionUser(), false, new EventDetails(EventUtils.CATEGORY.DATA, EventUtils.TYPE.WEB_SERVICE, Event.Modified, "", ""));
-            return new ResponseEntity<>(HttpStatus.OK);
+            return new ResponseEntity<>(new User(user), HttpStatus.OK);
         } catch (Exception e) {
             _log.error("Error occurred modifying user " + user.getLogin());
         }
@@ -613,6 +659,50 @@ public class UsersApi extends AbstractXapiRestController {
         return located;
     }
 
+    private void validateUser(final User model) throws DataFormatException, UserInitException, ResourceAlreadyExistsException {
+        final DataFormatException exception = new DataFormatException();
+
+        if (StringUtils.isBlank(model.getUsername())) {
+            exception.addMissing("username");
+        } else if (!Patterns.USERNAME.matcher(model.getUsername()).matches()) {
+            exception.addInvalid("username");
+        }
+
+        try {
+            final UserI user = getUserManagementService().getUser(model.getUsername());
+            if (user != null) {
+                throw new ResourceAlreadyExistsException("user", model.getUsername());
+            }
+        } catch (UserNotFoundException ignored) {
+            // This is actually what we want.
+        }
+
+        if (StringUtils.isBlank(model.getEmail())) {
+            exception.addMissing("email");
+        } else if (!Patterns.EMAIL.matcher(model.getEmail()).matches()) {
+            exception.addInvalid("email");
+        }
+
+        if (exception.hasDataFormatErrors()) {
+            throw exception;
+        }
+    }
+
+    private void fixPassword(final UserI user) throws PasswordComplexityException {
+        final String password = user.getPassword();
+        if (StringUtils.isNotBlank(password)) {
+            if (!_passwordValidator.isValid(password, user)) {
+                throw new PasswordComplexityException(_passwordValidator.getMessage());
+            }
+        } else {
+            user.setPassword(RandomStringUtils.randomAscii(32));
+        }
+        final String salt = Users.createNewSalt();
+        user.setPassword(new ShaPasswordEncoder(256).encodePassword(password, salt));
+        user.setPrimaryPassword_encrypt(true);
+        user.setSalt(salt);
+    }
+
     @SuppressWarnings("unused")
     public static class Event {
         public static String Added                 = "Added User";
diff --git a/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java b/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java
new file mode 100644
index 0000000000000000000000000000000000000000..8ac87ebd505983f97ed9e04cab0a9ac96095256d
--- /dev/null
+++ b/src/test/java/org/nrg/xapi/model/users/TestUserSerialization.java
@@ -0,0 +1,61 @@
+package org.nrg.xapi.model.users;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.nrg.framework.services.SerializerService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = TestUserSerializationConfig.class)
+public class TestUserSerialization {
+    @Autowired
+    public void setSerializer(final SerializerService serializer) {
+        _serializer = serializer;
+    }
+
+    @Test
+    public void testHiddenProperties() throws IOException {
+        final User input = new User();
+        input.setUsername("name");
+        input.setEmail("foo@bar.com");
+        input.setPassword("password");
+        input.setSalt("salt");
+        input.setAdmin(false);
+        input.setEnabled(true);
+
+        final String json = _serializer.toJson(input);
+        assertNotNull(json);
+        assertTrue(StringUtils.isNotBlank(json));
+
+        // Here's where we make sure the password and salt aren't serialized.
+        final JsonNode map = _serializer.deserializeJson(json);
+        assertNotNull(map);
+        assertTrue(map.has("username"));
+        assertTrue(map.has("email"));
+        assertFalse(map.has("password"));
+        assertFalse(map.has("salt"));
+        assertTrue(map.has("admin"));
+        assertTrue(map.has("enabled"));
+        assertFalse(map.has("verified"));
+
+        final User output = _serializer.deserializeJson(json, User.class);
+        assertNotNull(output);
+        assertTrue(StringUtils.isNotBlank(output.getUsername()));
+        assertTrue(StringUtils.isNotBlank(output.getEmail()));
+        assertTrue(StringUtils.isBlank(output.getPassword()));
+        assertTrue(StringUtils.isBlank(output.getSalt()));
+        assertFalse(output.isAdmin());
+        assertTrue(output.isEnabled());
+        assertNull(output.isVerified());
+    }
+
+    private SerializerService _serializer;
+}
diff --git a/src/test/java/org/nrg/xapi/model/users/TestUserSerializationConfig.java b/src/test/java/org/nrg/xapi/model/users/TestUserSerializationConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..a9b209fedc8f908b2ff7393720fd382b5d0c49be
--- /dev/null
+++ b/src/test/java/org/nrg/xapi/model/users/TestUserSerializationConfig.java
@@ -0,0 +1,38 @@
+package org.nrg.xapi.model.users;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module;
+import org.nrg.framework.beans.Beans;
+import org.nrg.framework.exceptions.NrgServiceException;
+import org.nrg.framework.services.SerializerService;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+import java.util.Map;
+
+@Configuration
+public class TestUserSerializationConfig {
+    @Bean
+    public Jackson2ObjectMapperBuilder objectMapperBuilder() throws NrgServiceException {
+        return new Jackson2ObjectMapperBuilder()
+                .serializationInclusion(JsonInclude.Include.NON_NULL)
+                .failOnEmptyBeans(false)
+                .mixIns(mixIns())
+                .featuresToEnable(JsonParser.Feature.ALLOW_SINGLE_QUOTES, JsonParser.Feature.ALLOW_YAML_COMMENTS)
+                .featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.WRITE_NULL_MAP_VALUES)
+                .modulesToInstall(new Hibernate4Module());
+    }
+
+    @Bean
+    public SerializerService serializerService(final Jackson2ObjectMapperBuilder objectMapperBuilder) {
+        return new SerializerService(objectMapperBuilder);
+    }
+
+    @Bean
+    public Map<Class<?>, Class<?>> mixIns() throws NrgServiceException {
+        return Beans.getMixIns();
+    }
+}
diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties
new file mode 100644
index 0000000000000000000000000000000000000000..f5b31a8e6bb75bc54d8881eb8f4db9ac53460aa7
--- /dev/null
+++ b/src/test/resources/log4j.properties
@@ -0,0 +1,14 @@
+#
+# log4j.properties
+# XNAT http://www.xnat.org
+# Copyright (c) 2016, Washington University School of Medicine
+# All Rights Reserved
+#
+# Released under the Simplified BSD.
+#
+log4j.rootLogger=DEBUG, test
+
+log4j.appender.test=org.nrg.log4j.NRGFileAppender
+log4j.appender.test.File=build/test-results/xnat-web-test.log
+log4j.appender.test.layout=org.apache.log4j.PatternLayout
+log4j.appender.test.layout.ConversionPattern=%p %t %c - %m%n