Fix "Failed to save config file (does the config have illegal objects in it?)" in Oxide plugins
Newtonsoft.Json cannot serialize a UnityEngine.Vector3 (or an entity reference) stored in your config class, so oxide/config never gets written. Store plain floats (x, y, z) or strings instead of Unity types.
ZoneMarkers v1.0.0: Failed to save config file (does the config have illegal objects in it?) (Self referencing loop detected for property 'normalized' with type 'UnityEngine.Vector3'. Path 'Zones[0].Position.normalized'.)
Why it happens
Oxide writes your config with Newtonsoft.Json. When a property in the config class is a Unity struct like Vector3, the serializer tries to walk every public property of that struct, including normalized, which itself returns a Vector3, so it recurses and reports "Self referencing loop detected for property 'normalized'". The same error appears when you put an entity (BaseEntity, BasePlayer) or a Quaternion in the config: the object graph has no natural end. The save call throws, Oxide logs <Name> v<Version>: Failed to save config file (does the config have illegal objects in it?) and the file on disk keeps the old contents, so edits to defaults never reach oxide/config/<Name>.json. Common triggers: a Vector3 Position field in the config class, a List<Vector3> of spawn points, a Quaternion rotation, or a saved entity reference.
How to fix it
- Open the config class and find any field whose type is a Unity struct (
Vector3,Quaternion,Color) or an entity/component type. - Replace them with serializable primitives: three floats
X,Y,Zfor a position, or strings like"120.5 8.0 -44.2"that you parse withVector3.Parse/string.Split. - If you want to keep
Vector3in code, mark it[JsonIgnore]and add a[JsonProperty]getter/setter pair that converts to and from floats; the getter must return a new serializable value, not the struct itself. - Delete the old oxide/config/ZoneMarkers.json so stale data does not fail deserialization.
- Run
oxide.reload ZoneMarkers. A successful save prints nothing; check that oxide/config/ZoneMarkers.json now contains your data and that the error line is gone on the next reload.
Before and after
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ZoneMarkers", "RustPluginGenerator", "1.0.0")]
[Description("Stores a list of named zone positions and prints them")]
public class ZoneMarkers : RustPlugin
{
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Zones")]
public List<Zone> Zones = new List<Zone>
{
new Zone { Name = "Spawn", Position = new Vector3(0f, 0f, 0f) }
};
}
private class Zone
{
[JsonProperty("Name")]
public string Name;
[JsonProperty("Position")]
public Vector3 Position;
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
config = Config.ReadObject<PluginConfig>();
if (config == null)
{
LoadDefaultConfig();
SaveConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(config);
[ChatCommand("zonelist")]
private void CmdZoneList(BasePlayer player, string command, string[] args)
{
if (player == null) return;
foreach (var zone in config.Zones)
SendReply(player, $"{zone.Name}: {zone.Position}");
}
}
}using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("ZoneMarkers", "RustPluginGenerator", "1.0.0")]
[Description("Stores a list of named zone positions and prints them")]
public class ZoneMarkers : RustPlugin
{
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Zones")]
public List<Zone> Zones = new List<Zone>
{
new Zone { Name = "Spawn", X = 0f, Y = 0f, Z = 0f }
};
}
private class Zone
{
[JsonProperty("Name")]
public string Name;
[JsonProperty("X")]
public float X;
[JsonProperty("Y")]
public float Y;
[JsonProperty("Z")]
public float Z;
[JsonIgnore]
public Vector3 Position => new Vector3(X, Y, Z);
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new System.Exception();
}
catch
{
PrintWarning("Configuration file is invalid, using defaults");
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
[ChatCommand("zonelist")]
private void CmdZoneList(BasePlayer player, string command, string[] args)
{
if (player == null || config == null) return;
foreach (var zone in config.Zones)
SendReply(player, $"{zone.Name}: {zone.Position}");
}
}
}FAQ
Can I keep using Vector3 in my config class?
Not directly. Newtonsoft can technically be configured with reference loop handling, but Oxide controls the serializer, so mark the field [JsonIgnore] and add [JsonProperty] float properties (X, Y, Z) that build a Vector3 in LoadConfig or in a helper method.
Does this error destroy my existing config?
No. The save throws before writing, so the file on disk is untouched. On the next load Oxide reads the old file; if its shape no longer matches your class you get the corrupt-config warning instead, so delete the file after changing the class.
Why does it mention 'normalized' and not Vector3?
normalized is a public property on UnityEngine.Vector3 that returns another Vector3, so the serializer follows it and hits itself. The property name in the message is a hint that the offending field is a Vector3, not a bug in your plugin logic.
Same problem with spawn points or rotations?
Yes. Use a small nested class with float fields (SpawnPoint { float X, Y, Z; }) or a list of Vector3 alternatives, and convert with new Vector3(p.X, p.Y, p.Z) at use time. Never store BaseEntity or BasePlayer references in config either.
Different error?
Paste the full console output and get the cause and a fix in plain English.
Open the error explainerSkip the debugging
Generated plugins are compiled before you get them, and compiler errors are fixed automatically.
Generate a plugin2 free plugins a month, no card.
Last updated Oct 26, 2026.
Get plugins that compile first time
Generate